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

refactor(core,schemas): rename password encryption to password digest

This commit is contained in:
wangsijie 2024-03-18 11:07:19 +08:00
parent 2ae8c112f5
commit 5e72e9fd5c
No known key found for this signature in database
GPG key ID: C72642FE24F7D42B
19 changed files with 136 additions and 120 deletions

View file

@ -1,5 +1,5 @@
import type { User } from '@logto/schemas'; 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'; import { pick } from '@silverhand/essentials';
export const mockUser: User = { export const mockUser: User = {
@ -8,9 +8,9 @@ export const mockUser: User = {
username: 'foo', username: 'foo',
primaryEmail: 'foo@logto.io', primaryEmail: 'foo@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: passwordDigest:
'$argon2i$v=19$m=4096,t=256,p=1$SYD0xSoVR8l+CN63Nz8fGw$ln5T09X9u4yd0DwLBKnlNV/eUHxwSWo32scw40ov4kI', '$argon2i$v=19$m=4096,t=256,p=1$SYD0xSoVR8l+CN63Nz8fGw$ln5T09X9u4yd0DwLBKnlNV/eUHxwSWo32scw40ov4kI',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, passwordAlgorithm: UsersPasswordAlgorithm.Argon2i,
name: null, name: null,
avatar: null, avatar: null,
identities: { identities: {
@ -58,15 +58,15 @@ export const mockUserWithMfaVerifications: User = {
export const mockUserResponse = pick(mockUser, ...userInfoSelectFields); export const mockUserResponse = pick(mockUser, ...userInfoSelectFields);
export const mockPasswordEncrypted = 'a1b2c3'; export const mockPasswordDigest = 'a1b2c3';
export const mockUserWithPassword: User = { export const mockUserWithPassword: User = {
tenantId: 'fake_tenant', tenantId: 'fake_tenant',
id: 'id', id: 'id',
username: 'username', username: 'username',
primaryEmail: 'foo@logto.io', primaryEmail: 'foo@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: mockPasswordEncrypted, passwordDigest: mockPasswordDigest,
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, passwordAlgorithm: UsersPasswordAlgorithm.Argon2i,
name: null, name: null,
avatar: null, avatar: null,
identities: { identities: {
@ -90,8 +90,8 @@ export const mockUserList: User[] = [
username: 'foo1', username: 'foo1',
primaryEmail: 'foo1@logto.io', primaryEmail: 'foo1@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: null, passwordDigest: null,
passwordEncryptionMethod: null, passwordAlgorithm: null,
name: null, name: null,
avatar: null, avatar: null,
identities: {}, identities: {},
@ -111,8 +111,8 @@ export const mockUserList: User[] = [
username: 'foo2', username: 'foo2',
primaryEmail: 'foo2@logto.io', primaryEmail: 'foo2@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: null, passwordDigest: null,
passwordEncryptionMethod: null, passwordAlgorithm: null,
name: null, name: null,
avatar: null, avatar: null,
identities: {}, identities: {},
@ -132,8 +132,8 @@ export const mockUserList: User[] = [
username: 'foo3', username: 'foo3',
primaryEmail: 'foo3@logto.io', primaryEmail: 'foo3@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: null, passwordDigest: null,
passwordEncryptionMethod: null, passwordAlgorithm: null,
name: null, name: null,
avatar: null, avatar: null,
identities: {}, identities: {},
@ -153,8 +153,8 @@ export const mockUserList: User[] = [
username: 'bar1', username: 'bar1',
primaryEmail: 'bar1@logto.io', primaryEmail: 'bar1@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: null, passwordDigest: null,
passwordEncryptionMethod: null, passwordAlgorithm: null,
name: null, name: null,
avatar: null, avatar: null,
identities: {}, identities: {},
@ -174,8 +174,8 @@ export const mockUserList: User[] = [
username: 'bar2', username: 'bar2',
primaryEmail: 'bar2@logto.io', primaryEmail: 'bar2@logto.io',
primaryPhone: '111111', primaryPhone: '111111',
passwordEncrypted: null, passwordDigest: null,
passwordEncryptionMethod: null, passwordAlgorithm: null,
name: null, name: null,
avatar: null, avatar: null,
identities: {}, identities: {},

View file

@ -1,4 +1,4 @@
import { MfaFactor, UsersPasswordEncryptionMethod } from '@logto/schemas'; import { MfaFactor, UsersPasswordAlgorithm } from '@logto/schemas';
import { createMockUtils } from '@logto/shared/esm'; import { createMockUtils } from '@logto/shared/esm';
import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js'; import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js';
@ -88,10 +88,10 @@ describe('generateUserId()', () => {
}); });
describe('encryptUserPassword()', () => { describe('encryptUserPassword()', () => {
it('generates salt, encrypted and method', async () => { it('generates salt, digest and method', async () => {
const { passwordEncryptionMethod, passwordEncrypted } = await encryptUserPassword('password'); const { passwordAlgorithm, passwordDigest } = await encryptUserPassword('password');
expect(passwordEncryptionMethod).toEqual(UsersPasswordEncryptionMethod.Argon2i); expect(passwordAlgorithm).toEqual(UsersPasswordAlgorithm.Argon2i);
expect(passwordEncrypted).toContain('argon2'); expect(passwordDigest).toContain('argon2');
}); });
}); });
@ -113,8 +113,8 @@ describe('verifyUserPassword()', () => {
describe('MD5', () => { describe('MD5', () => {
const user = { const user = {
...mockUser, ...mockUser,
passwordEncrypted: '5f4dcc3b5aa765d61d8327deb882cf99', passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.MD5, passwordAlgorithm: UsersPasswordAlgorithm.MD5,
}; };
it('resolves when password is correct', async () => { it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
@ -130,8 +130,8 @@ describe('verifyUserPassword()', () => {
describe('SHA1', () => { describe('SHA1', () => {
const user = { const user = {
...mockUser, ...mockUser,
passwordEncrypted: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', passwordDigest: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA1, passwordAlgorithm: UsersPasswordAlgorithm.SHA1,
}; };
it('resolves when password is correct', async () => { it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
@ -147,8 +147,8 @@ describe('verifyUserPassword()', () => {
describe('SHA256', () => { describe('SHA256', () => {
const user = { const user = {
...mockUser, ...mockUser,
passwordEncrypted: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', passwordDigest: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.SHA256, passwordAlgorithm: UsersPasswordAlgorithm.SHA256,
}; };
it('resolves when password is correct', async () => { it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
@ -164,8 +164,8 @@ describe('verifyUserPassword()', () => {
describe('Bcrypt', () => { describe('Bcrypt', () => {
const user = { const user = {
...mockUser, ...mockUser,
passwordEncrypted: '$2a$12$WQMqTfbtcZFBC1C1u8wpie6lXOSciUr5kk/8yEydoIMKltb9UKJ.6', passwordDigest: '$2a$12$WQMqTfbtcZFBC1C1u8wpie6lXOSciUr5kk/8yEydoIMKltb9UKJ.6',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Bcrypt, passwordAlgorithm: UsersPasswordAlgorithm.Bcrypt,
}; };
it('resolves when password is correct', async () => { it('resolves when password is correct', async () => {
await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError(); await expect(verifyUserPassword(user, 'password')).resolves.not.toThrowError();
@ -181,15 +181,15 @@ describe('verifyUserPassword()', () => {
describe('Migrate other algorithms to Argon2', () => { describe('Migrate other algorithms to Argon2', () => {
const user = { const user = {
...mockUser, ...mockUser,
passwordEncrypted: '5f4dcc3b5aa765d61d8327deb882cf99', passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordEncryptionMethod: UsersPasswordEncryptionMethod.MD5, passwordAlgorithm: UsersPasswordAlgorithm.MD5,
}; };
it('migrates password to Argon2', async () => { it('migrates password to Argon2', async () => {
await verifyUserPassword(user, 'password'); await verifyUserPassword(user, 'password');
expect(updateUserById).toHaveBeenCalledWith(user.id, { expect(updateUserById).toHaveBeenCalledWith(user.id, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
passwordEncrypted: expect.stringContaining('argon2'), passwordDigest: expect.stringContaining('argon2'),
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i, passwordAlgorithm: UsersPasswordAlgorithm.Argon2i,
}); });
}); });
}); });

View file

@ -1,5 +1,5 @@
import type { User, CreateUser, Scope, BindMfa, MfaVerification } from '@logto/schemas'; 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 { generateStandardShortId, generateStandardId } from '@logto/shared';
import type { Nullable } from '@silverhand/essentials'; import type { Nullable } from '@silverhand/essentials';
import { deduplicate } from '@silverhand/essentials'; import { deduplicate } from '@silverhand/essentials';
@ -18,13 +18,13 @@ import type { OmitAutoSetFields } from '#src/utils/sql.js';
export const encryptUserPassword = async ( export const encryptUserPassword = async (
password: string password: string
): Promise<{ ): Promise<{
passwordEncrypted: string; passwordDigest: string;
passwordEncryptionMethod: UsersPasswordEncryptionMethod; passwordAlgorithm: UsersPasswordAlgorithm;
}> => { }> => {
const passwordEncryptionMethod = UsersPasswordEncryptionMethod.Argon2i; const passwordAlgorithm = UsersPasswordAlgorithm.Argon2i;
const passwordEncrypted = await encryptPassword(password, passwordEncryptionMethod); 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<User>, password: string): Promise<User> => { const verifyUserPassword = async (user: Nullable<User>, password: string): Promise<User> => {
assertThat(user, new RequestError({ code: 'session.invalid_credentials', status: 422 })); assertThat(user, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
const { passwordEncrypted, passwordEncryptionMethod, id } = user; const { passwordDigest, passwordAlgorithm, id } = user;
assertThat( assertThat(
passwordEncrypted && passwordEncryptionMethod, passwordDigest && passwordAlgorithm,
new RequestError({ code: 'session.invalid_credentials', status: 422 }) new RequestError({ code: 'session.invalid_credentials', status: 422 })
); );
switch (passwordEncryptionMethod) { switch (passwordAlgorithm) {
case UsersPasswordEncryptionMethod.Argon2i: { case UsersPasswordAlgorithm.Argon2i: {
const result = await argon2Verify({ password, hash: passwordEncrypted }); const result = await argon2Verify({ password, hash: passwordDigest });
assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 })); assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
break; break;
} }
case UsersPasswordEncryptionMethod.MD5: { case UsersPasswordAlgorithm.MD5: {
const expectedEncrypted = await md5(password); const expectedDigest = await md5(password);
assertThat( assertThat(
expectedEncrypted === passwordEncrypted, expectedDigest === passwordDigest,
new RequestError({ code: 'session.invalid_credentials', status: 422 }) new RequestError({ code: 'session.invalid_credentials', status: 422 })
); );
break; break;
} }
case UsersPasswordEncryptionMethod.SHA1: { case UsersPasswordAlgorithm.SHA1: {
const expectedEncrypted = await sha1(password); const expectedDigest = await sha1(password);
assertThat( assertThat(
expectedEncrypted === passwordEncrypted, expectedDigest === passwordDigest,
new RequestError({ code: 'session.invalid_credentials', status: 422 }) new RequestError({ code: 'session.invalid_credentials', status: 422 })
); );
break; break;
} }
case UsersPasswordEncryptionMethod.SHA256: { case UsersPasswordAlgorithm.SHA256: {
const expectedEncrypted = await sha256(password); const expectedDigest = await sha256(password);
assertThat( assertThat(
expectedEncrypted === passwordEncrypted, expectedDigest === passwordDigest,
new RequestError({ code: 'session.invalid_credentials', status: 422 }) new RequestError({ code: 'session.invalid_credentials', status: 422 })
); );
break; break;
} }
case UsersPasswordEncryptionMethod.Bcrypt: { case UsersPasswordAlgorithm.Bcrypt: {
const result = await bcryptVerify({ password, hash: passwordEncrypted }); const result = await bcryptVerify({ password, hash: passwordDigest });
assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 })); assertThat(result, new RequestError({ code: 'session.invalid_credentials', status: 422 }));
break; break;
} }
@ -247,12 +247,12 @@ export const createUserLibrary = (queries: Queries) => {
} }
// Migrate password to default algorithm: argon2i // Migrate password to default algorithm: argon2i
if (passwordEncryptionMethod !== UsersPasswordEncryptionMethod.Argon2i) { if (passwordAlgorithm !== UsersPasswordAlgorithm.Argon2i) {
const { passwordEncrypted: newEncrypted, passwordEncryptionMethod: newMethod } = const { passwordDigest: newDigest, passwordAlgorithm: newAlgorithm } =
await encryptUserPassword(password); await encryptUserPassword(password);
return updateUserById(id, { return updateUserById(id, {
passwordEncrypted: newEncrypted, passwordDigest: newDigest,
passwordEncryptionMethod: newMethod, passwordAlgorithm: newAlgorithm,
}); });
} }

View file

@ -32,7 +32,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
const responseData = { const responseData = {
...pick(user, ...userInfoSelectFields), ...pick(user, ...userInfoSelectFields),
...conditional(user.passwordEncrypted && { hasPassword: Boolean(user.passwordEncrypted) }), ...conditional(user.passwordDigest && { hasPassword: Boolean(user.passwordDigest) }),
}; };
ctx.body = responseData; ctx.body = responseData;
@ -128,16 +128,16 @@ export default function userRoutes<T extends AuthedMeRouter>(
const { id: userId } = ctx.auth; const { id: userId } = ctx.auth;
const { password } = ctx.guard.body; 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 })); assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
if (oldPasswordEncrypted) { if (oldPasswordDigest) {
await checkVerificationStatus(userId); await checkVerificationStatus(userId);
} }
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(password);
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod }); await updateUserById(userId, { passwordDigest, passwordAlgorithm });
ctx.status = 204; ctx.status = 204;

View file

@ -1,5 +1,5 @@
import type { CreateUser, Role, SignInExperience, User } from '@logto/schemas'; 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 { createMockUtils, pickDefault } from '@logto/shared/esm';
import { removeUndefinedKeys } from '@silverhand/essentials'; import { removeUndefinedKeys } from '@silverhand/essentials';
@ -71,8 +71,8 @@ const { hasUser, findUserById, updateUserById, deleteUserIdentity, deleteUserByI
const { encryptUserPassword } = await mockEsmWithActual('#src/libraries/user.js', () => ({ const { encryptUserPassword } = await mockEsmWithActual('#src/libraries/user.js', () => ({
encryptUserPassword: jest.fn(() => ({ encryptUserPassword: jest.fn(() => ({
passwordEncrypted: 'password', passwordDigest: 'password',
passwordEncryptionMethod: 'Argon2i', passwordAlgorithm: 'Argon2i',
})), })),
})); }));
@ -143,7 +143,7 @@ describe('adminUserRoutes', () => {
username, username,
name, name,
passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, passwordAlgorithm: UsersPasswordAlgorithm.MD5,
}) })
).resolves.toHaveProperty('status', 200); ).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 () => { 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`); const response = await userRequest.get(`/users/foo/has-password`);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
expect(response.body).toEqual({ hasPassword: false }); expect(response.body).toEqual({ hasPassword: false });

View file

@ -1,6 +1,6 @@
import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
import { import {
UsersPasswordEncryptionMethod, UsersPasswordAlgorithm,
jsonObjectGuard, jsonObjectGuard,
userInfoSelectFields, userInfoSelectFields,
userProfileGuard, userProfileGuard,
@ -144,7 +144,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
username: string().regex(usernameRegEx), username: string().regex(usernameRegEx),
password: string().min(1), password: string().min(1),
passwordDigest: string(), passwordDigest: string(),
passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod), passwordAlgorithm: nativeEnum(UsersPasswordAlgorithm),
name: string(), name: string(),
avatar: string().url().or(literal('')).nullable(), avatar: string().url().or(literal('')).nullable(),
customData: jsonObjectGuard, customData: jsonObjectGuard,
@ -203,8 +203,8 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
...conditional(password && (await encryptUserPassword(password))), ...conditional(password && (await encryptUserPassword(password))),
...conditional( ...conditional(
passwordDigest && { passwordDigest && {
passwordEncrypted: passwordDigest, passwordDigest,
passwordEncryptionMethod: passwordAlgorithm, passwordAlgorithm,
} }
), ),
...conditional(profile && { profile }), ...conditional(profile && { profile }),
@ -266,11 +266,11 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
await findUserById(userId); await findUserById(userId);
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(password);
const user = await updateUserById(userId, { const user = await updateUserById(userId, {
passwordEncrypted, passwordDigest,
passwordEncryptionMethod, passwordAlgorithm,
}); });
ctx.body = pick(user, ...userInfoSelectFields); ctx.body = pick(user, ...userInfoSelectFields);
@ -313,7 +313,7 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
const user = await findUserById(userId); const user = await findUserById(userId);
ctx.body = { ctx.body = {
hasPassword: Boolean(user.passwordEncrypted), hasPassword: Boolean(user.passwordDigest),
}; };
return next(); return next();

View file

@ -26,8 +26,8 @@ const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () =>
mockEsm('#src/libraries/user.js', () => ({ mockEsm('#src/libraries/user.js', () => ({
encryptUserPassword: jest.fn().mockResolvedValue({ encryptUserPassword: jest.fn().mockResolvedValue({
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
}), }),
})); }));
@ -102,8 +102,8 @@ describe('submit action', () => {
username: 'username', username: 'username',
primaryPhone: '123456', primaryPhone: '123456',
primaryEmail: 'email@logto.io', primaryEmail: 'email@logto.io',
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
identities: { identities: {
logto: { userId: userInfo.id, details: userInfo }, logto: { userId: userInfo.id, details: userInfo },
}, },

View file

@ -27,8 +27,8 @@ const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () =>
const { encryptUserPassword } = mockEsm('#src/libraries/user.js', () => ({ const { encryptUserPassword } = mockEsm('#src/libraries/user.js', () => ({
encryptUserPassword: jest.fn().mockResolvedValue({ encryptUserPassword: jest.fn().mockResolvedValue({
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
}), }),
})); }));
@ -103,8 +103,8 @@ describe('submit action', () => {
username: 'username', username: 'username',
primaryPhone: '123456', primaryPhone: '123456',
primaryEmail: 'email@logto.io', primaryEmail: 'email@logto.io',
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
identities: { identities: {
logto: { userId: userInfo.id, details: userInfo }, logto: { userId: userInfo.id, details: userInfo },
}, },
@ -312,8 +312,8 @@ describe('submit action', () => {
expect(getLogtoConnectorById).toBeCalledWith('logto'); expect(getLogtoConnectorById).toBeCalledWith('logto');
expect(updateUserById).toBeCalledWith('foo', { expect(updateUserById).toBeCalledWith('foo', {
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
identities: { identities: {
logto: { userId: userInfo.id, details: userInfo }, logto: { userId: userInfo.id, details: userInfo },
google: { userId: 'googleId', details: {} }, google: { userId: 'googleId', details: {} },
@ -341,8 +341,8 @@ describe('submit action', () => {
await submitInteraction(interaction, ctx, tenant); await submitInteraction(interaction, ctx, tenant);
expect(updateUserById).toBeCalledWith('foo', { expect(updateUserById).toBeCalledWith('foo', {
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
identities: { identities: {
logto: { userId: userInfo.id, details: userInfo }, logto: { userId: userInfo.id, details: userInfo },
google: { userId: 'googleId', details: {} }, google: { userId: 'googleId', details: {} },
@ -394,8 +394,8 @@ describe('submit action', () => {
expect(encryptUserPassword).toBeCalledWith('password'); expect(encryptUserPassword).toBeCalledWith('password');
expect(updateUserById).toBeCalledWith('foo', { expect(updateUserById).toBeCalledWith('foo', {
passwordEncrypted: 'passwordEncrypted', passwordDigest: 'passwordDigest',
passwordEncryptionMethod: 'plain', passwordAlgorithm: 'plain',
}); });
expect(assignInteractionResults).not.toBeCalled(); expect(assignInteractionResults).not.toBeCalled();

View file

@ -248,11 +248,9 @@ export default async function submitInteraction(
} }
// Forgot Password // Forgot Password
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword( const { passwordDigest, passwordAlgorithm } = await encryptUserPassword(profile.password);
profile.password
);
await updateUserById(accountId, { passwordEncrypted, passwordEncryptionMethod }); await updateUserById(accountId, { passwordDigest, passwordAlgorithm });
ctx.assignInteractionHookResult({ userId: accountId }); ctx.assignInteractionHookResult({ userId: accountId });
await clearInteractionStorage(ctx, provider); await clearInteractionStorage(ctx, provider);
ctx.status = 204; ctx.status = 204;

View file

@ -22,8 +22,8 @@ export const isSocialIdentifier = (
// Social identities can take place the role of password // Social identities can take place the role of password
export const isUserPasswordSet = ({ export const isUserPasswordSet = ({
passwordEncrypted, passwordDigest,
identities, identities,
}: Pick<User, 'passwordEncrypted' | 'identities'>): boolean => { }: Pick<User, 'passwordDigest' | 'identities'>): boolean => {
return Boolean(passwordEncrypted) || Object.keys(identities).length > 0; return Boolean(passwordDigest) || Object.keys(identities).length > 0;
}; };

View file

@ -9,7 +9,7 @@ import type { Identifier } from '../types/index.js';
const { jest } = import.meta; const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest); 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 } }); const tenantContext = new MockTenant(undefined, { users: { findUserById } });

View file

@ -192,11 +192,11 @@ export default async function verifyProfile(
const passwordProfile = passwordProfileResult.data; const passwordProfile = passwordProfileResult.data;
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(accountId); const { passwordDigest: oldPasswordDigest } = await findUserById(accountId);
assertThat( assertThat(
!oldPasswordEncrypted || !oldPasswordDigest ||
!(await argon2Verify({ password: passwordProfile.password, hash: oldPasswordEncrypted })), !(await argon2Verify({ password: passwordProfile.password, hash: oldPasswordDigest })),
new RequestError({ code: 'user.same_password', status: 422 }) new RequestError({ code: 'user.same_password', status: 422 })
); );

View file

@ -1,6 +1,6 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { UsersPasswordEncryptionMethod } from '@logto/schemas'; import { UsersPasswordAlgorithm } from '@logto/schemas';
import { argon2i } from 'hash-wasm'; import { argon2i } from 'hash-wasm';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -8,10 +8,10 @@ import assertThat from '#src/utils/assert-that.js';
export const encryptPassword = async ( export const encryptPassword = async (
password: string, password: string,
method: UsersPasswordEncryptionMethod method: UsersPasswordAlgorithm
): Promise<string> => { ): Promise<string> => {
assertThat( assertThat(
method === UsersPasswordEncryptionMethod.Argon2i, method === UsersPasswordAlgorithm.Argon2i,
new RequestError({ code: 'password.unsupported_encryption_method', method }) new RequestError({ code: 'password.unsupported_encryption_method', method })
); );

View file

@ -6,7 +6,7 @@ import type {
Role, Role,
User, User,
UserSsoIdentity, UserSsoIdentity,
UsersPasswordEncryptionMethod, UsersPasswordAlgorithm,
} from '@logto/schemas'; } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
@ -19,7 +19,7 @@ export type CreateUserPayload = Partial<{
password: string; password: string;
name: string; name: string;
passwordDigest: string; passwordDigest: string;
passwordAlgorithm: UsersPasswordEncryptionMethod; passwordAlgorithm: UsersPasswordAlgorithm;
}>; }>;
export const createUser = async (payload: CreateUserPayload = {}) => export const createUser = async (payload: CreateUserPayload = {}) =>

View file

@ -2,11 +2,7 @@ import fs from 'node:fs/promises';
import { createServer, type RequestListener } from 'node:http'; import { createServer, type RequestListener } from 'node:http';
import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit'; import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit';
import { import { type UserProfile, type JsonObject, type UsersPasswordAlgorithm } from '@logto/schemas';
type UserProfile,
type JsonObject,
type UsersPasswordEncryptionMethod,
} from '@logto/schemas';
import { RequestError } from 'got'; import { RequestError } from 'got';
import { createUser } from '#src/api/index.js'; import { createUser } from '#src/api/index.js';
@ -20,7 +16,7 @@ export const createUserByAdmin = async (
primaryPhone?: string; primaryPhone?: string;
name?: string; name?: string;
passwordDigest?: string; passwordDigest?: string;
passwordAlgorithm?: UsersPasswordEncryptionMethod; passwordAlgorithm?: UsersPasswordAlgorithm;
customData?: JsonObject; customData?: JsonObject;
profile?: UserProfile; profile?: UserProfile;
} = {} } = {}

View file

@ -1,6 +1,6 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { UsersPasswordEncryptionMethod, ConnectorType } from '@logto/schemas'; import { UsersPasswordAlgorithm, ConnectorType } from '@logto/schemas';
import { HTTPError } from 'got'; import { HTTPError } from 'got';
import { import {
@ -61,7 +61,7 @@ describe('admin console user management', () => {
it('should create user with password digest successfully', async () => { it('should create user with password digest successfully', async () => {
const user = await createUserByAdmin({ const user = await createUserByAdmin({
passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, passwordAlgorithm: UsersPasswordAlgorithm.MD5,
}); });
await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow(); await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow();

View file

@ -2,7 +2,7 @@ import {
InteractionEvent, InteractionEvent,
ConnectorType, ConnectorType,
SignInIdentifier, SignInIdentifier,
UsersPasswordEncryptionMethod, UsersPasswordAlgorithm,
} from '@logto/schemas'; } from '@logto/schemas';
import { import {
@ -64,7 +64,7 @@ describe('Sign-in flow using password identifiers', () => {
const user = await createUserByAdmin({ const user = await createUserByAdmin({
username, username,
passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99', passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordAlgorithm: UsersPasswordEncryptionMethod.MD5, passwordAlgorithm: UsersPasswordAlgorithm.MD5,
}); });
const client = await initClient(); const client = await initClient();

View file

@ -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;

View file

@ -1,6 +1,6 @@
/* init_order = 1 */ /* 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 ( create table users (
tenant_id varchar(21) not null tenant_id varchar(21) not null
@ -9,8 +9,8 @@ create table users (
username varchar(128), username varchar(128),
primary_email varchar(128), primary_email varchar(128),
primary_phone varchar(128), primary_phone varchar(128),
password_encrypted varchar(128), password_digest varchar(128),
password_encryption_method users_password_encryption_method, password_algorithm users_password_algorithm,
name varchar(128), name varchar(128),
/** The URL that points to the user's profile picture. Mapped to OpenID Connect's `picture` claim. */ /** The URL that points to the user's profile picture. Mapped to OpenID Connect's `picture` claim. */
avatar varchar(2048), avatar varchar(2048),