From d56bc2f7314b75e45777b44bbe54c2cc97531dfc Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 6 Aug 2024 15:12:47 +0800 Subject: [PATCH] feat(core,schemas): add support for argon2d and argon2id (#6404) --- .changeset/thirty-cups-joke.md | 10 +++++ packages/core/src/libraries/user.test.ts | 39 ++++++++++++++++++- packages/core/src/libraries/user.ts | 5 ++- .../next-1722926389-argon2d-argon2id.ts | 35 +++++++++++++++++ packages/schemas/tables/users.sql | 2 +- 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 .changeset/thirty-cups-joke.md create mode 100644 packages/schemas/alterations/next-1722926389-argon2d-argon2id.ts diff --git a/.changeset/thirty-cups-joke.md b/.changeset/thirty-cups-joke.md new file mode 100644 index 000000000..e616dec83 --- /dev/null +++ b/.changeset/thirty-cups-joke.md @@ -0,0 +1,10 @@ +--- +"@logto/schemas": minor +"@logto/core": minor +--- + +add support for new password digest algorithm argon2d and argon2id + +In `POST /users`, the `passwordAlgorithm` field now accepts `Argon2d` and `Argon2id`. + +Users with those algorithms will be migrated to `Argon2i` upon succussful sign in. diff --git a/packages/core/src/libraries/user.test.ts b/packages/core/src/libraries/user.test.ts index b7dec3996..edb7ebbf0 100644 --- a/packages/core/src/libraries/user.test.ts +++ b/packages/core/src/libraries/user.test.ts @@ -99,7 +99,7 @@ describe('encryptUserPassword()', () => { describe('verifyUserPassword()', () => { const { verifyUserPassword } = createUserLibrary(queries); - describe('Argon2', () => { + describe('Argon2i', () => { it('resolves when password is correct', async () => { await expect(verifyUserPassword(mockUser, 'password')).resolves.not.toThrowError(); }); @@ -111,6 +111,43 @@ describe('verifyUserPassword()', () => { }); }); + describe('Argon2d', () => { + const user = { + ...mockUser, + passwordEncrypted: '$argon2d$v=19$m=16,t=2,p=1$VW1JcEJrMjN1Vnp3Tm5JUA$Ddl/I6Zem7vbZ4r5jPCb/g', + passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2d, + }; + + 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('Argon2id', () => { + const user = { + ...mockUser, + passwordEncrypted: + '$argon2id$v=19$m=16,t=2,p=1$VW1JcEJrMjN1Vnp3Tm5JUA$0uzNwxbjs/f/1e5r4uX7JQ', + passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2id, + }; + + 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('MD5', () => { const user = { ...mockUser, diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 43996fbd1..071f9f92c 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -178,7 +178,10 @@ export const createUserLibrary = (queries: Queries) => { ); switch (passwordEncryptionMethod) { - case UsersPasswordEncryptionMethod.Argon2i: { + // Argon2i, Argon2id, Argon2d shares the same verify function + case UsersPasswordEncryptionMethod.Argon2i: + case UsersPasswordEncryptionMethod.Argon2id: + case UsersPasswordEncryptionMethod.Argon2d: { const result = await argon2Verify({ password, hash: passwordEncrypted }); assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 })); break; diff --git a/packages/schemas/alterations/next-1722926389-argon2d-argon2id.ts b/packages/schemas/alterations/next-1722926389-argon2d-argon2id.ts new file mode 100644 index 000000000..550919725 --- /dev/null +++ b/packages/schemas/alterations/next-1722926389-argon2d-argon2id.ts @@ -0,0 +1,35 @@ +import { sql } from '@silverhand/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 'Argon2id'; + alter type users_password_encryption_method add value 'Argon2d'; + `); + }, + down: async (pool) => { + const { rows } = await pool.query(sql` + select id from users + where password_encryption_method = ${'Argon2id'} + or password_encryption_method = ${'Argon2d'} + `); + if (rows.length > 0) { + throw new Error('There are users with password encryption methods Argon2id or Argon2d.'); + } + + await pool.query(sql` + create type users_password_encryption_method_revised as enum ('Argon2i', 'SHA1', 'SHA256', 'MD5', 'Bcrypt'); + + 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; diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index 66294bba2..e0304208f 100644 --- a/packages/schemas/tables/users.sql +++ b/packages/schemas/tables/users.sql @@ -1,6 +1,6 @@ /* init_order = 1 */ -create type users_password_encryption_method as enum ('Argon2i', 'SHA1', 'SHA256', 'MD5', 'Bcrypt'); +create type users_password_encryption_method as enum ('Argon2i', 'Argon2id', 'Argon2d', 'SHA1', 'SHA256', 'MD5', 'Bcrypt'); create table users ( tenant_id varchar(21) not null