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:
parent
791f6f9013
commit
79b49ab79a
6 changed files with 166 additions and 7 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue