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 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: {},
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 },
|
||||||
},
|
},
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 } });
|
||||||
|
|
||||||
|
|
|
@ -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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 = {}) =>
|
||||||
|
|
|
@ -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;
|
||||||
} = {}
|
} = {}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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 */
|
/* 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),
|
||||||
|
|
Loading…
Reference in a new issue