0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(core): support more encrypt methods (#5444)

* feat(schemas): add more encryption methods

* feat(core): support more encrypt methods

* fix(schemas): fix alter down column name

* fix(core): fix tiny lint issue

* refactor(core,schemas): use uppercase value

* feat(core,schemas): add bcrypt

* fix(schemas): fix alter script

* refactor(core,schemas): rename bcrypt and use hash-wasm

* chore: fix lock file
This commit is contained in:
wangsijie 2024-03-04 12:18:19 +09:00 committed by GitHub
parent 791f6f9013
commit 79b49ab79a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 166 additions and 7 deletions

View file

@ -8,7 +8,8 @@ export const mockUser: User = {
username: 'foo',
primaryEmail: 'foo@logto.io',
primaryPhone: '111111',
passwordEncrypted: 'password',
passwordEncrypted:
'$argon2i$v=19$m=4096,t=256,p=1$SYD0xSoVR8l+CN63Nz8fGw$ln5T09X9u4yd0DwLBKnlNV/eUHxwSWo32scw40ov4kI',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i,
name: null,
avatar: null,

View file

@ -2,11 +2,12 @@ import { MfaFactor, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js';
import { mockUser } from '#src/__mocks__/user.js';
import RequestError from '#src/errors/RequestError/index.js';
import { MockQueries } from '#src/test-utils/tenant.js';
const { jest } = import.meta;
const { encryptUserPassword, createUserLibrary } = await import('./user.js');
const { encryptUserPassword, createUserLibrary, verifyUserPassword } = await import('./user.js');
const hasUserWithId = jest.fn();
const updateUserById = jest.fn();
@ -68,6 +69,90 @@ describe('encryptUserPassword()', () => {
});
});
describe('verifyUserPassword()', () => {
describe('Argon2', () => {
it('resolves when password is correct', async () => {
await expect(
verifyUserPassword(mockUser, 'HOH2hTmW0xtYAJUfRSQjJdW5')
).resolves.not.toThrowError();
});
it('rejects when password is incorrect', async () => {
await expect(verifyUserPassword(mockUser, 'wrong')).rejects.toThrowError(
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
});
});
describe('MD5', () => {
const user = {
...mockUser,
passwordEncrypted: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.MD5,
};
it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
});
it('rejects when password is incorrect', async () => {
await expect(verifyUserPassword(user, 'wrong')).rejects.toThrowError(
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
});
});
describe('SHA1', () => {
const user = {
...mockUser,
passwordEncrypted: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA1,
};
it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
});
it('rejects when password is incorrect', async () => {
await expect(verifyUserPassword(user, 'wrong')).rejects.toThrowError(
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
});
});
describe('SHA256', () => {
const user = {
...mockUser,
passwordEncrypted: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA256,
};
it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
});
it('rejects when password is incorrect', async () => {
await expect(verifyUserPassword(user, 'wrong')).rejects.toThrowError(
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
});
});
describe('Bcrypt', () => {
const user = {
...mockUser,
passwordEncrypted: '$2a$12$WQMqTfbtcZFBC1C1u8wpie6lXOSciUr5kk/8yEydoIMKltb9UKJ.6',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Bcrypt,
};
it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
});
it('rejects when password is incorrect', async () => {
await expect(verifyUserPassword(user, 'wrong')).rejects.toThrowError(
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
});
});
});
describe('findUserScopesForResourceId()', () => {
const { findUserScopesForResourceIndicator } = createUserLibrary(queries);

View file

@ -4,7 +4,7 @@ import { generateStandardShortId, generateStandardId } from '@logto/shared';
import type { OmitAutoSetFields } from '@logto/shared';
import type { Nullable } from '@silverhand/essentials';
import { deduplicate } from '@silverhand/essentials';
import { argon2Verify } from 'hash-wasm';
import { argon2Verify, bcryptVerify, md5, sha1, sha256 } from 'hash-wasm';
import pRetry from 'p-retry';
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
@ -40,9 +40,47 @@ export const verifyUserPassword = async (user: Nullable<User>, password: string)
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
const result = await argon2Verify({ password, hash: passwordEncrypted });
switch (passwordEncryptionMethod) {
case UsersPasswordEncryptionMethod.Argon2i: {
const result = await argon2Verify({ password, hash: passwordEncrypted });
assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
break;
}
case UsersPasswordEncryptionMethod.MD5: {
const expectedEncrypted = await md5(password);
assertThat(
expectedEncrypted === passwordEncrypted,
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
break;
}
case UsersPasswordEncryptionMethod.SHA1: {
const expectedEncrypted = await sha1(password);
assertThat(
expectedEncrypted === passwordEncrypted,
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
break;
}
case UsersPasswordEncryptionMethod.SHA256: {
const expectedEncrypted = await sha256(password);
assertThat(
expectedEncrypted === passwordEncrypted,
new RequestError({ code: 'session.invalid_credentials', status: 422 })
);
break;
}
case UsersPasswordEncryptionMethod.Bcrypt: {
const result = await bcryptVerify({ password, hash: passwordEncrypted });
assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
break;
}
default: {
throw new RequestError({ code: 'session.invalid_credentials', status: 422 });
}
}
assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
// TODO(@sijie) migrate to use argon2
return user;
};

View file

@ -11,7 +11,6 @@ export const encryptPassword = async (
method: UsersPasswordEncryptionMethod
): Promise<string> => {
assertThat(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
method === UsersPasswordEncryptionMethod.Argon2i,
new RequestError({ code: 'password.unsupported_encryption_method', method })
);

View file

@ -0,0 +1,36 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter type users_password_encryption_method add value 'SHA1';
alter type users_password_encryption_method add value 'SHA256';
alter type users_password_encryption_method add value 'MD5';
alter type users_password_encryption_method add value 'Bcrypt';
`);
},
down: async (pool) => {
const { rows } = await pool.query(sql`
select id from users
where password_encryption_method <> ${'Argon2i'}
`);
if (rows.length > 0) {
throw new Error('There are users with password encryption methods other than Argon2i.');
}
await pool.query(sql`
create type users_password_encryption_method_revised as enum ('Argon2i');
alter table users
alter column password_encryption_method type users_password_encryption_method_revised
using password_encryption_method::text::users_password_encryption_method_revised;
drop type users_password_encryption_method;
alter type users_password_encryption_method_revised rename to users_password_encryption_method;
`);
},
};
export default alteration;

View file

@ -1,6 +1,6 @@
/* init_order = 1 */
create type users_password_encryption_method as enum ('Argon2i');
create type users_password_encryption_method as enum ('Argon2i', 'SHA1', 'SHA256', 'MD5', 'Bcrypt');
create table users (
tenant_id varchar(21) not null