0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

feat(core): create user with encrypted password (#5446)

This commit is contained in:
wangsijie 2024-03-04 15:41:36 +09:00 committed by GitHub
parent 94d3b2c2e7
commit cc01acbd0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 141 additions and 5 deletions

View file

@ -0,0 +1,8 @@
---
"@logto/integration-tests": minor
"@logto/phrases": minor
"@logto/schemas": minor
"@logto/core": minor
---
Create a new user through API with password digest and corresponding algorithm

View file

@ -124,6 +124,15 @@
},
"username": {
"description": "Username for the user. It should be unique across all users."
},
"password": {
"description": "Plain text password for the user."
},
"passwordDigest": {
"description": "In case you already have the password digests and not the passwords, you can use them for the newly created user via this property. The value should be generated with one of the supported algorithms. The algorithm can be specified using the `passwordAlgorithm` property."
},
"passwordAlgorithm": {
"description": "The hash algorithm used for the password. It should be one of the supported algorithms: argon2, md5, sha1, sha256. Should the encryption algorithm differ from argon2, it will automatically be upgraded to argon2 upon the user's next sign-in."
}
}
}

View file

@ -1,5 +1,5 @@
import type { CreateUser, Role, SignInExperience, User } from '@logto/schemas';
import { RoleType } from '@logto/schemas';
import { RoleType, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { createMockUtils, pickDefault } from '@logto/shared/esm';
import { removeUndefinedKeys } from '@silverhand/essentials';
@ -136,6 +136,20 @@ describe('adminUserRoutes', () => {
).resolves.toHaveProperty('status', 200);
});
it('POST /users with password digest', async () => {
const username = 'MJAtLogto';
const name = 'Michael';
await expect(
userRequest.post('/users').send({
username,
name,
passwordDigest: '5f4dcc3b5aa765d61d8327deb882cf99',
passwordAlgorithm: UsersPasswordEncryptionMethod.MD5,
})
).resolves.toHaveProperty('status', 200);
});
it('POST /users should throw if username exists', async () => {
const mockHasUser = hasUser as jest.Mock;
mockHasUser.mockImplementationOnce(async () => true);

View file

@ -1,7 +1,12 @@
import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
import { jsonObjectGuard, userInfoSelectFields, userProfileResponseGuard } from '@logto/schemas';
import {
UsersPasswordEncryptionMethod,
jsonObjectGuard,
userInfoSelectFields,
userProfileResponseGuard,
} from '@logto/schemas';
import { conditional, pick, yes } from '@silverhand/essentials';
import { boolean, literal, object, string } from 'zod';
import { boolean, literal, nativeEnum, object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import { encryptUserPassword, verifyUserPassword } from '#src/libraries/user.js';
@ -111,13 +116,26 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
primaryEmail: string().regex(emailRegEx),
username: string().regex(usernameRegEx),
password: string().min(1),
passwordDigest: string(),
passwordAlgorithm: nativeEnum(UsersPasswordEncryptionMethod),
name: string(),
}).partial(),
response: userProfileResponseGuard,
status: [200, 404, 422],
}),
async (ctx, next) => {
const { primaryEmail, primaryPhone, username, password, name } = ctx.guard.body;
const {
primaryEmail,
primaryPhone,
username,
password,
name,
passwordDigest,
passwordAlgorithm,
} = ctx.guard.body;
assertThat(!(password && passwordDigest), new RequestError('user.password_and_digest'));
assertThat(!passwordDigest || passwordAlgorithm, 'user.password_algorithm_required');
assertThat(
!username || !(await hasUser(username)),
@ -148,6 +166,12 @@ export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: R
username,
name,
...conditional(password && (await encryptUserPassword(password))),
...conditional(
passwordDigest && {
passwordEncrypted: passwordDigest,
passwordEncryptionMethod: passwordAlgorithm,
}
),
},
[]
);

View file

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

View file

@ -2,6 +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 UsersPasswordEncryptionMethod } from '@logto/schemas';
import { RequestError } from 'got';
import { createUser } from '#src/api/index.js';
@ -12,7 +13,9 @@ export const createUserByAdmin = async (
password?: string,
primaryEmail?: string,
primaryPhone?: string,
name?: string
name?: string,
passwordDigest?: string,
passwordAlgorithm?: UsersPasswordEncryptionMethod
) => {
return createUser({
username: username ?? generateUsername(),
@ -20,6 +23,8 @@ export const createUserByAdmin = async (
name: name ?? username ?? 'John',
primaryEmail,
primaryPhone,
passwordDigest,
passwordAlgorithm,
});
};

View file

@ -1,3 +1,4 @@
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
import { HTTPError } from 'got';
import {
@ -36,6 +37,20 @@ describe('admin console user management', () => {
expect(userDetailsWithSsoIdentities.ssoIdentities).toStrictEqual([]);
});
it('should create user with password digest successfully', async () => {
const user = await createUserByAdmin(
undefined,
undefined,
undefined,
undefined,
undefined,
'5f4dcc3b5aa765d61d8327deb882cf99',
UsersPasswordEncryptionMethod.MD5
);
await expect(verifyUserPassword(user.id, 'password')).resolves.not.toThrow();
});
it('should fail when create user with conflict identifiers', async () => {
const [username, password, email, phone] = [
generateUsername(),

View file

@ -37,6 +37,10 @@ const user = {
missing_mfa: 'Sie müssen zusätzliches MFA verbinden, bevor Sie sich anmelden können.',
totp_already_in_use: 'TOTP wird bereits verwendet.',
backup_code_already_in_use: 'Backup-Code wird bereits verwendet.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -32,6 +32,8 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
password_algorithm_required: 'Password algorithm is required.',
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -34,6 +34,10 @@ const user = {
missing_mfa: 'Debes vincular un MFA adicional antes de iniciar sesión.',
totp_already_in_use: 'TOTP ya está en uso.',
backup_code_already_in_use: 'El código de respaldo ya está en uso.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'Vous devez lier un MFA supplémentaire avant de vous connecter.',
totp_already_in_use: 'TOTP est déjà utilisé.',
backup_code_already_in_use: 'Le code de sauvegarde est déjà utilisé.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: "Devi legare un'ulteriore MFA prima di accedere.",
totp_already_in_use: 'TOTP è già in uso.',
backup_code_already_in_use: 'Il codice di backup è già in uso.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'MFAを追加してからサインインしてください。',
totp_already_in_use: 'TOTPはすでに使用されています。',
backup_code_already_in_use: 'バックアップコードはすでに使用されています。',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -32,6 +32,10 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'Você precisa vincular MFA adicional antes de fazer login.',
totp_already_in_use: 'TOTP já está em uso.',
backup_code_already_in_use: 'O código de backup já está em uso.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -33,6 +33,10 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -32,6 +32,10 @@ const user = {
missing_mfa: 'You need to bind additional MFA before signing-in.',
totp_already_in_use: 'TOTP is already in use.',
backup_code_already_in_use: 'Backup code is already in use.',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -31,6 +31,10 @@ const user = {
missing_mfa: '你需要在登录之前绑定额外的MFA。',
totp_already_in_use: 'TOTP已在使用中。',
backup_code_already_in_use: '备用代码已在使用中。',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -31,6 +31,10 @@ const user = {
missing_mfa: '你需要在登錄前綁定額外的 MFA。',
totp_already_in_use: 'TOTP 已經在使用中。',
backup_code_already_in_use: '備份代碼已經在使用中。',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);

View file

@ -31,6 +31,10 @@ const user = {
missing_mfa: '在登錄前需要綁定額外的多因素驗證。',
totp_already_in_use: 'TOTP 已經在使用中。',
backup_code_already_in_use: '備份代碼已經在使用中。',
/** UNTRANSLATED */
password_algorithm_required: 'Password algorithm is required.',
/** UNTRANSLATED */
password_and_digest: 'You cannot set both plain text password and password digest.',
};
export default Object.freeze(user);