From 5e72e9fd5c3197aa26ac11363e06fe0fe9c3b7ba Mon Sep 17 00:00:00 2001 From: wangsijie Date: Mon, 18 Mar 2024 11:07:19 +0800 Subject: [PATCH] refactor(core,schemas): rename password encryption to password digest --- packages/core/src/__mocks__/user.ts | 32 ++++++------ packages/core/src/libraries/user.test.ts | 34 ++++++------ packages/core/src/libraries/user.ts | 52 +++++++++---------- packages/core/src/routes-me/user.ts | 10 ++-- .../core/src/routes/admin-user/basics.test.ts | 10 ++-- packages/core/src/routes/admin-user/basics.ts | 16 +++--- .../actions/submit-interaction.mfa.test.ts | 8 +-- .../actions/submit-interaction.test.ts | 20 +++---- .../interaction/actions/submit-interaction.ts | 6 +-- .../src/routes/interaction/utils/index.ts | 6 +-- ...ofile-verification.forgot-password.test.ts | 2 +- .../verifications/profile-verification.ts | 6 +-- packages/core/src/utils/password.ts | 6 +-- .../integration-tests/src/api/admin-user.ts | 4 +- .../integration-tests/src/helpers/index.ts | 8 +-- .../src/tests/api/admin-user.test.ts | 4 +- .../happy-path.test.ts | 4 +- .../next-1711685199-password-algorithm.ts | 22 ++++++++ packages/schemas/tables/users.sql | 6 +-- 19 files changed, 136 insertions(+), 120 deletions(-) create mode 100644 packages/schemas/alterations/next-1711685199-password-algorithm.ts diff --git a/packages/core/src/__mocks__/user.ts b/packages/core/src/__mocks__/user.ts index 14bfe03f1..b0aa58198 100644 --- a/packages/core/src/__mocks__/user.ts +++ b/packages/core/src/__mocks__/user.ts @@ -1,5 +1,5 @@ import type { User } from '@logto/schemas'; -import { MfaFactor, userInfoSelectFields, UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { MfaFactor, userInfoSelectFields, UsersPasswordAlgorithm } from '@logto/schemas'; import { pick } from '@silverhand/essentials'; export const mockUser: User = { @@ -8,9 +8,9 @@ export const mockUser: User = { username: 'foo', primaryEmail: 'foo@logto.io', primaryPhone: '111111', - passwordEncrypted: + passwordDigest: '$argon2i$v=19$m=4096,t=256,p=1$SYD0xSoVR8l+CN63Nz8fGw$ln5T09X9u4yd0DwLBKnlNV/eUHxwSWo32scw40ov4kI', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, + passwordAlgorithm: UsersPasswordAlgorithm.Argon2i, name: null, avatar: null, identities: { @@ -58,15 +58,15 @@ export const mockUserWithMfaVerifications: User = { export const mockUserResponse = pick(mockUser, ...userInfoSelectFields); -export const mockPasswordEncrypted = 'a1b2c3'; +export const mockPasswordDigest = 'a1b2c3'; export const mockUserWithPassword: User = { tenantId: 'fake_tenant', id: 'id', username: 'username', primaryEmail: 'foo@logto.io', primaryPhone: '111111', - passwordEncrypted: mockPasswordEncrypted, - passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, + passwordDigest: mockPasswordDigest, + passwordAlgorithm: UsersPasswordAlgorithm.Argon2i, name: null, avatar: null, identities: { @@ -90,8 +90,8 @@ export const mockUserList: User[] = [ username: 'foo1', primaryEmail: 'foo1@logto.io', primaryPhone: '111111', - passwordEncrypted: null, - passwordEncryptionMethod: null, + passwordDigest: null, + passwordAlgorithm: null, name: null, avatar: null, identities: {}, @@ -111,8 +111,8 @@ export const mockUserList: User[] = [ username: 'foo2', primaryEmail: 'foo2@logto.io', primaryPhone: '111111', - passwordEncrypted: null, - passwordEncryptionMethod: null, + passwordDigest: null, + passwordAlgorithm: null, name: null, avatar: null, identities: {}, @@ -132,8 +132,8 @@ export const mockUserList: User[] = [ username: 'foo3', primaryEmail: 'foo3@logto.io', primaryPhone: '111111', - passwordEncrypted: null, - passwordEncryptionMethod: null, + passwordDigest: null, + passwordAlgorithm: null, name: null, avatar: null, identities: {}, @@ -153,8 +153,8 @@ export const mockUserList: User[] = [ username: 'bar1', primaryEmail: 'bar1@logto.io', primaryPhone: '111111', - passwordEncrypted: null, - passwordEncryptionMethod: null, + passwordDigest: null, + passwordAlgorithm: null, name: null, avatar: null, identities: {}, @@ -174,8 +174,8 @@ export const mockUserList: User[] = [ username: 'bar2', primaryEmail: 'bar2@logto.io', primaryPhone: '111111', - passwordEncrypted: null, - passwordEncryptionMethod: null, + passwordDigest: null, + passwordAlgorithm: null, name: null, avatar: null, identities: {}, diff --git a/packages/core/src/libraries/user.test.ts b/packages/core/src/libraries/user.test.ts index fcd18cbfa..a28c5dacd 100644 --- a/packages/core/src/libraries/user.test.ts +++ b/packages/core/src/libraries/user.test.ts @@ -1,4 +1,4 @@ -import { MfaFactor, UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { MfaFactor, UsersPasswordAlgorithm } from '@logto/schemas'; import { createMockUtils } from '@logto/shared/esm'; import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js'; @@ -88,10 +88,10 @@ describe('generateUserId()', () => { }); describe('encryptUserPassword()', () => { - it('generates salt, encrypted and method', async () => { - const { passwordEncryptionMethod, passwordEncrypted } = await encryptUserPassword('password'); - expect(passwordEncryptionMethod).toEqual(UsersPasswordEncryptionMethod.Argon2i); - expect(passwordEncrypted).toContain('argon2'); + it('generates salt, digest and method', async () => { + const { passwordAlgorithm, passwordDigest } = await encryptUserPassword('password'); + expect(passwordAlgorithm).toEqual(UsersPasswordAlgorithm.Argon2i); + expect(passwordDigest).toContain('argon2'); }); }); @@ -113,8 +113,8 @@ describe('verifyUserPassword()', () => { describe('MD5', () => { const user = { ...mockUser, - passwordEncrypted: '5f4dcc3b5aa765d61d8327deb882cf99', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.MD5, + passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', + passwordAlgorithm: UsersPasswordAlgorithm.MD5, }; it('resolves when password is correct', async () => { await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); @@ -130,8 +130,8 @@ describe('verifyUserPassword()', () => { describe('SHA1', () => { const user = { ...mockUser, - passwordEncrypted: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA1, + passwordDigest: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', + passwordAlgorithm: UsersPasswordAlgorithm.SHA1, }; it('resolves when password is correct', async () => { await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); @@ -147,8 +147,8 @@ describe('verifyUserPassword()', () => { describe('SHA256', () => { const user = { ...mockUser, - passwordEncrypted: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA256, + passwordDigest: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + passwordAlgorithm: UsersPasswordAlgorithm.SHA256, }; it('resolves when password is correct', async () => { await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); @@ -164,8 +164,8 @@ describe('verifyUserPassword()', () => { describe('Bcrypt', () => { const user = { ...mockUser, - passwordEncrypted: '$2a$12$WQMqTfbtcZFBC1C1u8wpie6lXOSciUr5kk/8yEydoIMKltb9UKJ.6', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.Bcrypt, + passwordDigest: '$2a$12$WQMqTfbtcZFBC1C1u8wpie6lXOSciUr5kk/8yEydoIMKltb9UKJ.6', + passwordAlgorithm: UsersPasswordAlgorithm.Bcrypt, }; it('resolves when password is correct', async () => { await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); @@ -181,15 +181,15 @@ describe('verifyUserPassword()', () => { describe('Migrate other algorithms to Argon2', () => { const user = { ...mockUser, - passwordEncrypted: '5f4dcc3b5aa765d61d8327deb882cf99', - passwordEncryptionMethod: UsersPasswordEncryptionMethod.MD5, + passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', + passwordAlgorithm: UsersPasswordAlgorithm.MD5, }; it('migrates password to Argon2', async () => { await verifyUserPassword(user, 'password'); expect(updateUserById).toHaveBeenCalledWith(user.id, { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - passwordEncrypted: expect.stringContaining('argon2'), - passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, + passwordDigest: expect.stringContaining('argon2'), + passwordAlgorithm: UsersPasswordAlgorithm.Argon2i, }); }); }); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 027999ff5..363cd0ccd 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -1,5 +1,5 @@ import type { User, CreateUser, Scope, BindMfa, MfaVerification } from '@logto/schemas'; -import { MfaFactor, Users, UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { MfaFactor, Users, UsersPasswordAlgorithm } from '@logto/schemas'; import { generateStandardShortId, generateStandardId } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; import { deduplicate } from '@silverhand/essentials'; @@ -18,13 +18,13 @@ import type { OmitAutoSetFields } from '#src/utils/sql.js'; export const encryptUserPassword = async ( password: string ): Promise<{ - passwordEncrypted: string; - passwordEncryptionMethod: UsersPasswordEncryptionMethod; + passwordDigest: string; + passwordAlgorithm: UsersPasswordAlgorithm; }> => { - const passwordEncryptionMethod = UsersPasswordEncryptionMethod.Argon2i; - const passwordEncrypted = await encryptPassword(password, passwordEncryptionMethod); + const passwordAlgorithm = UsersPasswordAlgorithm.Argon2i; + const passwordDigest = await encryptPassword(password, passwordAlgorithm); - return { passwordEncrypted, passwordEncryptionMethod }; + return { passwordDigest, passwordAlgorithm }; }; /** @@ -199,45 +199,45 @@ export const createUserLibrary = (queries: Queries) => { const verifyUserPassword = async (user: Nullable, password: string): Promise => { assertThat(user, new RequestError({ code: 'session.invalid_credentials', status: 422 })); - const { passwordEncrypted, passwordEncryptionMethod, id } = user; + const { passwordDigest, passwordAlgorithm, id } = user; assertThat( - passwordEncrypted && passwordEncryptionMethod, + passwordDigest && passwordAlgorithm, new RequestError({ code: 'session.invalid_credentials', status: 422 }) ); - switch (passwordEncryptionMethod) { - case UsersPasswordEncryptionMethod.Argon2i: { - const result = await argon2Verify({ password, hash: passwordEncrypted }); + switch (passwordAlgorithm) { + case UsersPasswordAlgorithm.Argon2i: { + const result = await argon2Verify({ password, hash: passwordDigest }); assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 })); break; } - case UsersPasswordEncryptionMethod.MD5: { - const expectedEncrypted = await md5(password); + case UsersPasswordAlgorithm.MD5: { + const expectedDigest = await md5(password); assertThat( - expectedEncrypted === passwordEncrypted, + expectedDigest === passwordDigest, new RequestError({ code: 'session.invalid_credentials', status: 422 }) ); break; } - case UsersPasswordEncryptionMethod.SHA1: { - const expectedEncrypted = await sha1(password); + case UsersPasswordAlgorithm.SHA1: { + const expectedDigest = await sha1(password); assertThat( - expectedEncrypted === passwordEncrypted, + expectedDigest === passwordDigest, new RequestError({ code: 'session.invalid_credentials', status: 422 }) ); break; } - case UsersPasswordEncryptionMethod.SHA256: { - const expectedEncrypted = await sha256(password); + case UsersPasswordAlgorithm.SHA256: { + const expectedDigest = await sha256(password); assertThat( - expectedEncrypted === passwordEncrypted, + expectedDigest === passwordDigest, new RequestError({ code: 'session.invalid_credentials', status: 422 }) ); break; } - case UsersPasswordEncryptionMethod.Bcrypt: { - const result = await bcryptVerify({ password, hash: passwordEncrypted }); + case UsersPasswordAlgorithm.Bcrypt: { + const result = await bcryptVerify({ password, hash: passwordDigest }); assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 })); break; } @@ -247,12 +247,12 @@ export const createUserLibrary = (queries: Queries) => { } // Migrate password to default algorithm: argon2i - if (passwordEncryptionMethod !== UsersPasswordEncryptionMethod.Argon2i) { - const { passwordEncrypted: newEncrypted, passwordEncryptionMethod: newMethod } = + if (passwordAlgorithm !== UsersPasswordAlgorithm.Argon2i) { + const { passwordDigest: newDigest, passwordAlgorithm: newAlgorithm } = await encryptUserPassword(password); return updateUserById(id, { - passwordEncrypted: newEncrypted, - passwordEncryptionMethod: newMethod, + passwordDigest: newDigest, + passwordAlgorithm: newAlgorithm, }); } diff --git a/packages/core/src/routes-me/user.ts b/packages/core/src/routes-me/user.ts index c1e3faf9c..f1ef29073 100644 --- a/packages/core/src/routes-me/user.ts +++ b/packages/core/src/routes-me/user.ts @@ -32,7 +32,7 @@ export default function userRoutes( const responseData = { ...pick(user, ...userInfoSelectFields), - ...conditional(user.passwordEncrypted && { hasPassword: Boolean(user.passwordEncrypted) }), + ...conditional(user.passwordDigest && { hasPassword: Boolean(user.passwordDigest) }), }; ctx.body = responseData; @@ -128,16 +128,16 @@ export default function userRoutes( const { id: userId } = ctx.auth; const { password } = ctx.guard.body; - const { isSuspended, passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId); + const { isSuspended, passwordDigest: oldPasswordDigest } = await findUserById(userId); assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); - if (oldPasswordEncrypted) { + if (oldPasswordDigest) { await checkVerificationStatus(userId); } - const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); - await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod }); + const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(password); + await updateUserById(userId, { passwordDigest, passwordAlgorithm }); ctx.status = 204; diff --git a/packages/core/src/routes/admin-user/basics.test.ts b/packages/core/src/routes/admin-user/basics.test.ts index 280502541..bc93d9aa2 100644 --- a/packages/core/src/routes/admin-user/basics.test.ts +++ b/packages/core/src/routes/admin-user/basics.test.ts @@ -1,5 +1,5 @@ import type { CreateUser, Role, SignInExperience, User } from '@logto/schemas'; -import { RoleType, UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { RoleType, UsersPasswordAlgorithm } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; import { removeUndefinedKeys } from '@silverhand/essentials'; @@ -71,8 +71,8 @@ const { hasUser, findUserById, updateUserById, deleteUserIdentity, deleteUserByI const { encryptUserPassword } = await mockEsmWithActual('#src/libraries/user.js', () => ({ encryptUserPassword: jest.fn(() => ({ - passwordEncrypted: 'password', - passwordEncryptionMethod: 'Argon2i', + passwordDigest: 'password', + passwordAlgorithm: 'Argon2i', })), })); @@ -143,7 +143,7 @@ describe('adminUserRoutes', () => { username, name, passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', - passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, + passwordAlgorithm: UsersPasswordAlgorithm.MD5, }) ).resolves.toHaveProperty('status', 200); }); @@ -365,7 +365,7 @@ describe('adminUserRoutes', () => { }); it('GET /users/:userId/has-password should return false if user does not have password', async () => { - findUserById.mockImplementationOnce(async () => ({ ...mockUser, passwordEncrypted: null })); + findUserById.mockImplementationOnce(async () => ({ ...mockUser, passwordDigest: null })); const response = await userRequest.get(`/users/foo/has-password`); expect(response.status).toEqual(200); expect(response.body).toEqual({ hasPassword: false }); diff --git a/packages/core/src/routes/admin-user/basics.ts b/packages/core/src/routes/admin-user/basics.ts index 9ab6624e7..7ce7fa50b 100644 --- a/packages/core/src/routes/admin-user/basics.ts +++ b/packages/core/src/routes/admin-user/basics.ts @@ -1,6 +1,6 @@ import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { - UsersPasswordEncryptionMethod, + UsersPasswordAlgorithm, jsonObjectGuard, userInfoSelectFields, userProfileGuard, @@ -144,7 +144,7 @@ export default function adminUserBasicsRoutes(...args: R username: string().regex(usernameRegEx), password: string().min(1), passwordDigest: string(), - passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod), + passwordAlgorithm: nativeEnum(UsersPasswordAlgorithm), name: string(), avatar: string().url().or(literal('')).nullable(), customData: jsonObjectGuard, @@ -203,8 +203,8 @@ export default function adminUserBasicsRoutes(...args: R ...conditional(password && (await encryptUserPassword(password))), ...conditional( passwordDigest && { - passwordEncrypted: passwordDigest, - passwordEncryptionMethod: passwordAlgorithm, + passwordDigest, + passwordAlgorithm, } ), ...conditional(profile && { profile }), @@ -266,11 +266,11 @@ export default function adminUserBasicsRoutes(...args: R await findUserById(userId); - const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); + const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(password); const user = await updateUserById(userId, { - passwordEncrypted, - passwordEncryptionMethod, + passwordDigest, + passwordAlgorithm, }); ctx.body = pick(user, ...userInfoSelectFields); @@ -313,7 +313,7 @@ export default function adminUserBasicsRoutes(...args: R const user = await findUserById(userId); ctx.body = { - hasPassword: Boolean(user.passwordEncrypted), + hasPassword: Boolean(user.passwordDigest), }; return next(); diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.mfa.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.mfa.test.ts index 9d3fbe9ae..4daa8d691 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.mfa.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.mfa.test.ts @@ -26,8 +26,8 @@ const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () => mockEsm('#src/libraries/user.js', () => ({ encryptUserPassword: jest.fn().mockResolvedValue({ - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', }), })); @@ -102,8 +102,8 @@ describe('submit action', () => { username: 'username', primaryPhone: '123456', primaryEmail: 'email@logto.io', - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', identities: { logto: { userId: userInfo.id, details: userInfo }, }, diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index bc9007ac8..a3beb9dcc 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -27,8 +27,8 @@ const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () => const { encryptUserPassword } = mockEsm('#src/libraries/user.js', () => ({ encryptUserPassword: jest.fn().mockResolvedValue({ - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', }), })); @@ -103,8 +103,8 @@ describe('submit action', () => { username: 'username', primaryPhone: '123456', primaryEmail: 'email@logto.io', - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', identities: { logto: { userId: userInfo.id, details: userInfo }, }, @@ -312,8 +312,8 @@ describe('submit action', () => { expect(getLogtoConnectorById).toBeCalledWith('logto'); expect(updateUserById).toBeCalledWith('foo', { - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', identities: { logto: { userId: userInfo.id, details: userInfo }, google: { userId: 'googleId', details: {} }, @@ -341,8 +341,8 @@ describe('submit action', () => { await submitInteraction(interaction, ctx, tenant); expect(updateUserById).toBeCalledWith('foo', { - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', identities: { logto: { userId: userInfo.id, details: userInfo }, google: { userId: 'googleId', details: {} }, @@ -394,8 +394,8 @@ describe('submit action', () => { expect(encryptUserPassword).toBeCalledWith('password'); expect(updateUserById).toBeCalledWith('foo', { - passwordEncrypted: 'passwordEncrypted', - passwordEncryptionMethod: 'plain', + passwordDigest: 'passwordDigest', + passwordAlgorithm: 'plain', }); expect(assignInteractionResults).not.toBeCalled(); diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 7f4583c56..2aa36dd4c 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -248,11 +248,9 @@ export default async function submitInteraction( } // Forgot Password - const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword( - profile.password - ); + const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(profile.password); - await updateUserById(accountId, { passwordEncrypted, passwordEncryptionMethod }); + await updateUserById(accountId, { passwordDigest, passwordAlgorithm }); ctx.assignInteractionHookResult({ userId: accountId }); await clearInteractionStorage(ctx, provider); ctx.status = 204; diff --git a/packages/core/src/routes/interaction/utils/index.ts b/packages/core/src/routes/interaction/utils/index.ts index be0b00526..50b9bcb22 100644 --- a/packages/core/src/routes/interaction/utils/index.ts +++ b/packages/core/src/routes/interaction/utils/index.ts @@ -22,8 +22,8 @@ export const isSocialIdentifier = ( // Social identities can take place the role of password export const isUserPasswordSet = ({ - passwordEncrypted, + passwordDigest, identities, -}: Pick): boolean => { - return Boolean(passwordEncrypted) || Object.keys(identities).length > 0; +}: Pick): boolean => { + return Boolean(passwordDigest) || Object.keys(identities).length > 0; }; diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts index 702debd27..6d0e3b573 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts @@ -9,7 +9,7 @@ import type { Identifier } from '../types/index.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); -const findUserById = jest.fn().mockResolvedValue({ id: 'foo', passwordEncrypted: 'passwordHash' }); +const findUserById = jest.fn().mockResolvedValue({ id: 'foo', passwordDigest: 'passwordHash' }); const tenantContext = new MockTenant(undefined, { users: { findUserById } }); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts index 066b40ce0..dc8ef1ae1 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts @@ -192,11 +192,11 @@ export default async function verifyProfile( const passwordProfile = passwordProfileResult.data; - const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(accountId); + const { passwordDigest: oldPasswordDigest } = await findUserById(accountId); assertThat( - !oldPasswordEncrypted || - !(await argon2Verify({ password: passwordProfile.password, hash: oldPasswordEncrypted })), + !oldPasswordDigest || + !(await argon2Verify({ password: passwordProfile.password, hash: oldPasswordDigest })), new RequestError({ code: 'user.same_password', status: 422 }) ); diff --git a/packages/core/src/utils/password.ts b/packages/core/src/utils/password.ts index e5eb46201..dc54b853d 100644 --- a/packages/core/src/utils/password.ts +++ b/packages/core/src/utils/password.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; -import { UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { UsersPasswordAlgorithm } from '@logto/schemas'; import { argon2i } from 'hash-wasm'; import RequestError from '#src/errors/RequestError/index.js'; @@ -8,10 +8,10 @@ import assertThat from '#src/utils/assert-that.js'; export const encryptPassword = async ( password: string, - method: UsersPasswordEncryptionMethod + method: UsersPasswordAlgorithm ): Promise => { assertThat( - method === UsersPasswordEncryptionMethod.Argon2i, + method === UsersPasswordAlgorithm.Argon2i, new RequestError({ code: 'password.unsupported_encryption_method', method }) ); diff --git a/packages/integration-tests/src/api/admin-user.ts b/packages/integration-tests/src/api/admin-user.ts index f9dd9d371..4fca040f6 100644 --- a/packages/integration-tests/src/api/admin-user.ts +++ b/packages/integration-tests/src/api/admin-user.ts @@ -6,7 +6,7 @@ import type { Role, User, UserSsoIdentity, - UsersPasswordEncryptionMethod, + UsersPasswordAlgorithm, } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; @@ -19,7 +19,7 @@ export type CreateUserPayload = Partial<{ password: string; name: string; passwordDigest: string; - passwordAlgorithm: UsersPasswordEncryptionMethod; + passwordAlgorithm: UsersPasswordAlgorithm; }>; export const createUser = async (payload: CreateUserPayload = {}) => diff --git a/packages/integration-tests/src/helpers/index.ts b/packages/integration-tests/src/helpers/index.ts index d31c5ddf6..ac9d68a70 100644 --- a/packages/integration-tests/src/helpers/index.ts +++ b/packages/integration-tests/src/helpers/index.ts @@ -2,11 +2,7 @@ import fs from 'node:fs/promises'; import { createServer, type RequestListener } from 'node:http'; import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit'; -import { - type UserProfile, - type JsonObject, - type UsersPasswordEncryptionMethod, -} from '@logto/schemas'; +import { type UserProfile, type JsonObject, type UsersPasswordAlgorithm } from '@logto/schemas'; import { RequestError } from 'got'; import { createUser } from '#src/api/index.js'; @@ -20,7 +16,7 @@ export const createUserByAdmin = async ( primaryPhone?: string; name?: string; passwordDigest?: string; - passwordAlgorithm?: UsersPasswordEncryptionMethod; + passwordAlgorithm?: UsersPasswordAlgorithm; customData?: JsonObject; profile?: UserProfile; } = {} diff --git a/packages/integration-tests/src/tests/api/admin-user.test.ts b/packages/integration-tests/src/tests/api/admin-user.test.ts index 789a01ec6..ac90b94ca 100644 --- a/packages/integration-tests/src/tests/api/admin-user.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.test.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; -import { UsersPasswordEncryptionMethod, ConnectorType } from '@logto/schemas'; +import { UsersPasswordAlgorithm, ConnectorType } from '@logto/schemas'; import { HTTPError } from 'got'; import { @@ -61,7 +61,7 @@ describe('admin console user management', () => { it('should create user with password digest successfully', async () => { const user = await createUserByAdmin({ passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', - passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, + passwordAlgorithm: UsersPasswordAlgorithm.MD5, }); await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow(); diff --git a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier/happy-path.test.ts b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier/happy-path.test.ts index dc872ae0c..554c96c98 100644 --- a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier/happy-path.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier/happy-path.test.ts @@ -2,7 +2,7 @@ import { InteractionEvent, ConnectorType, SignInIdentifier, - UsersPasswordEncryptionMethod, + UsersPasswordAlgorithm, } from '@logto/schemas'; import { @@ -64,7 +64,7 @@ describe('Sign-in flow using password identifiers', () => { const user = await createUserByAdmin({ username, passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', - passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, + passwordAlgorithm: UsersPasswordAlgorithm.MD5, }); const client = await initClient(); diff --git a/packages/schemas/alterations/next-1711685199-password-algorithm.ts b/packages/schemas/alterations/next-1711685199-password-algorithm.ts new file mode 100644 index 000000000..dba2b0856 --- /dev/null +++ b/packages/schemas/alterations/next-1711685199-password-algorithm.ts @@ -0,0 +1,22 @@ +import { sql } from '@silverhand/slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table users rename column password_encrypted to password_digest; + alter table users rename column password_encryption_method to password_algorithm; + alter type users_password_encryption_method rename to users_password_algorithm; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter type users_password_algorithm rename to users_password_encryption_method; + alter table users rename column password_digest to password_encrypted; + alter table users rename column password_algorithm to password_encryption_method; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index 66294bba2..60b0ee7e8 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_algorithm as enum ('Argon2i', 'SHA1', 'SHA256', 'MD5', 'Bcrypt'); create table users ( tenant_id varchar(21) not null @@ -9,8 +9,8 @@ create table users ( username varchar(128), primary_email varchar(128), primary_phone varchar(128), - password_encrypted varchar(128), - password_encryption_method users_password_encryption_method, + password_digest varchar(128), + password_algorithm users_password_algorithm, name varchar(128), /** The URL that points to the user's profile picture. Mapped to OpenID Connect's `picture` claim. */ avatar varchar(2048),