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:
parent
2ae8c112f5
commit
5e72e9fd5c
19 changed files with 136 additions and 120 deletions
|
@ -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: {},
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<User>, password: string): Promise<User> => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
|
||||
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<T extends AuthedMeRouter>(
|
|||
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;
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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<T extends AuthedRouter>(...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<T extends AuthedRouter>(...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<T extends AuthedRouter>(...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<T extends AuthedRouter>(...args: R
|
|||
const user = await findUserById(userId);
|
||||
|
||||
ctx.body = {
|
||||
hasPassword: Boolean(user.passwordEncrypted),
|
||||
hasPassword: Boolean(user.passwordDigest),
|
||||
};
|
||||
|
||||
return next();
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -22,8 +22,8 @@ export const isSocialIdentifier = (
|
|||
|
||||
// Social identities can take place the role of password
|
||||
export const isUserPasswordSet = ({
|
||||
passwordEncrypted,
|
||||
passwordDigest,
|
||||
identities,
|
||||
}: Pick<User, 'passwordEncrypted' | 'identities'>): boolean => {
|
||||
return Boolean(passwordEncrypted) || Object.keys(identities).length > 0;
|
||||
}: Pick<User, 'passwordDigest' | 'identities'>): boolean => {
|
||||
return Boolean(passwordDigest) || Object.keys(identities).length > 0;
|
||||
};
|
||||
|
|
|
@ -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 } });
|
||||
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
|
||||
|
|
|
@ -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<string> => {
|
||||
assertThat(
|
||||
method === UsersPasswordEncryptionMethod.Argon2i,
|
||||
method === UsersPasswordAlgorithm.Argon2i,
|
||||
new RequestError({ code: 'password.unsupported_encryption_method', method })
|
||||
);
|
||||
|
||||
|
|
|
@ -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 = {}) =>
|
||||
|
|
|
@ -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;
|
||||
} = {}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue