mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): password checking api (#6567)
* feat(core): password checking api * refactor(core): improve API response
This commit is contained in:
parent
b639249159
commit
cc346b4e0a
9 changed files with 321 additions and 12 deletions
34
.changeset/yellow-yaks-bathe.md
Normal file
34
.changeset/yellow-yaks-bathe.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
"@logto/core": minor
|
||||||
|
"@logto/integration-tests": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add password policy checking api
|
||||||
|
|
||||||
|
Add `POST /api/sign-in-exp/default/check-password` API to check if the password meets the password policy configured in the default sign-in experience. A user ID is required for this API if rejects user info is enabled in the password policy.
|
||||||
|
|
||||||
|
Here's a non-normative example of the request and response:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/sign-in-exp/default/check-password
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "123",
|
||||||
|
"userId": "some-user-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```http
|
||||||
|
400 Bad Request
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"result": false,
|
||||||
|
"issues": [
|
||||||
|
{ "code": "password_rejected.too_short" },
|
||||||
|
{ "code": "password_rejected.character_types" },
|
||||||
|
{ "code": "password_rejected.restricted.sequence" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -1,4 +1,4 @@
|
||||||
import { emailRegEx, usernameRegEx } from '@logto/core-kit';
|
import { emailRegEx, PasswordPolicyChecker, usernameRegEx } from '@logto/core-kit';
|
||||||
import { userInfoSelectFields, jsonObjectGuard } from '@logto/schemas';
|
import { userInfoSelectFields, jsonObjectGuard } from '@logto/schemas';
|
||||||
import { conditional, pick } from '@silverhand/essentials';
|
import { conditional, pick } from '@silverhand/essentials';
|
||||||
import { literal, object, string } from 'zod';
|
import { literal, object, string } from 'zod';
|
||||||
|
@ -9,6 +9,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
|
||||||
import type { RouterInitArgs } from '../routes/types.js';
|
import type { RouterInitArgs } from '../routes/types.js';
|
||||||
|
import { checkPasswordPolicyForUser } from '../utils/password.js';
|
||||||
|
|
||||||
import type { AuthedMeRouter } from './types.js';
|
import type { AuthedMeRouter } from './types.js';
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
||||||
const {
|
const {
|
||||||
queries: {
|
queries: {
|
||||||
users: { findUserById, updateUserById },
|
users: { findUserById, updateUserById },
|
||||||
|
signInExperiences: { findDefaultSignInExperience },
|
||||||
},
|
},
|
||||||
libraries: {
|
libraries: {
|
||||||
users: { checkIdentifierCollision, verifyUserPassword },
|
users: { checkIdentifierCollision, verifyUserPassword },
|
||||||
|
@ -118,7 +120,6 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
||||||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||||
|
|
||||||
await verifyUserPassword(user, password);
|
await verifyUserPassword(user, password);
|
||||||
|
|
||||||
await createVerificationStatus(userId, null);
|
await createVerificationStatus(userId, null);
|
||||||
|
|
||||||
ctx.status = 204;
|
ctx.status = 204;
|
||||||
|
@ -129,16 +130,25 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/password',
|
'/password',
|
||||||
koaGuard({ body: object({ password: string().min(1) }) }),
|
koaGuard({ body: object({ password: string().min(1) }), status: [204, 400, 401] }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { id: userId } = ctx.auth;
|
const { id: userId } = ctx.auth;
|
||||||
const { password } = ctx.guard.body;
|
const { password } = ctx.guard.body;
|
||||||
|
|
||||||
const { isSuspended } = await findUserById(userId);
|
const user = await findUserById(userId);
|
||||||
|
|
||||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||||
|
|
||||||
await checkVerificationStatus(userId, null);
|
const [signInExperience] = await Promise.all([
|
||||||
|
findDefaultSignInExperience(),
|
||||||
|
checkVerificationStatus(userId, null),
|
||||||
|
]);
|
||||||
|
const passwordPolicyChecker = new PasswordPolicyChecker(signInExperience.passwordPolicy);
|
||||||
|
const issues = await checkPasswordPolicyForUser(passwordPolicyChecker, password, user);
|
||||||
|
|
||||||
|
if (issues.length > 0) {
|
||||||
|
throw new RequestError('password.rejected', { issues });
|
||||||
|
}
|
||||||
|
|
||||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||||
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod });
|
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod });
|
||||||
|
|
|
@ -132,6 +132,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/sign-in-exp/default/check-password": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "CheckPasswordWithDefaultSignInExperience",
|
||||||
|
"summary": "Check if a password meets the password policy",
|
||||||
|
"description": "Check if a password meets the password policy in the sign-in experience settings.",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"description": "The password to check."
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"description": "The user ID to check the password for. It is required if rejects user info is enabled in the password policy."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The password meets the password policy."
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "The password does not meet the password policy or no user ID provided."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { DemoConnector } from '@logto/connector-kit';
|
import { DemoConnector } from '@logto/connector-kit';
|
||||||
|
import { PasswordPolicyChecker } from '@logto/core-kit';
|
||||||
import { ConnectorType, SignInExperiences } from '@logto/schemas';
|
import { ConnectorType, SignInExperiences } from '@logto/schemas';
|
||||||
|
import { tryThat } from '@silverhand/essentials';
|
||||||
import { literal, object, string, z } from 'zod';
|
import { literal, object, string, z } from 'zod';
|
||||||
|
|
||||||
import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js';
|
import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js';
|
||||||
import { validateMfa } from '#src/libraries/sign-in-experience/mfa.js';
|
import { validateMfa } from '#src/libraries/sign-in-experience/mfa.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
|
||||||
|
import RequestError from '../../errors/RequestError/index.js';
|
||||||
|
import { checkPasswordPolicyForUser } from '../../utils/password.js';
|
||||||
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
|
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
|
||||||
|
|
||||||
import customUiAssetsRoutes from './custom-ui-assets/index.js';
|
import customUiAssetsRoutes from './custom-ui-assets/index.js';
|
||||||
|
@ -16,6 +20,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
||||||
const [router, { queries, libraries, connectors }] = args;
|
const [router, { queries, libraries, connectors }] = args;
|
||||||
const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences;
|
const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences;
|
||||||
const { deleteConnectorById } = queries.connectors;
|
const { deleteConnectorById } = queries.connectors;
|
||||||
|
const { findUserById } = queries.users;
|
||||||
const {
|
const {
|
||||||
signInExperiences: { validateLanguageInfo },
|
signInExperiences: { validateLanguageInfo },
|
||||||
quota: { guardTenantUsageByKey, reportSubscriptionUpdatesUsage },
|
quota: { guardTenantUsageByKey, reportSubscriptionUpdatesUsage },
|
||||||
|
@ -125,5 +130,51 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/sign-in-exp/default/check-password',
|
||||||
|
koaGuard({
|
||||||
|
body: z.object({ password: z.string(), userId: z.string().optional() }),
|
||||||
|
response: z.object({ result: z.literal(true) }).or(
|
||||||
|
z.object({
|
||||||
|
result: z.literal(false),
|
||||||
|
issues: z.array(
|
||||||
|
z.object({ code: z.string(), interpolation: z.record(z.unknown()).optional() })
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
status: [200, 400],
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const { password, userId } = ctx.guard.body;
|
||||||
|
const [signInExperience, user] = await Promise.all([
|
||||||
|
findDefaultSignInExperience(),
|
||||||
|
userId && findUserById(userId),
|
||||||
|
]);
|
||||||
|
const passwordPolicyChecker = new PasswordPolicyChecker(signInExperience.passwordPolicy);
|
||||||
|
|
||||||
|
const issues = await tryThat(
|
||||||
|
async () =>
|
||||||
|
user
|
||||||
|
? checkPasswordPolicyForUser(passwordPolicyChecker, password, user)
|
||||||
|
: passwordPolicyChecker.check(password),
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
throw new RequestError('request.invalid_input', { message: error.message });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (issues.length > 0) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = { result: false, issues };
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = { result: true };
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
customUiAssetsRoutes(...args);
|
customUiAssetsRoutes(...args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,12 @@ export const throwByDifference = (builtCustomRoutes: Set<string>) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Path segments that are treated as namespace prefixes. */
|
/** Path segments that are treated as namespace prefixes. */
|
||||||
const namespacePrefixes = Object.freeze(['jit', '.well-known', 'experience']);
|
const namespacePrefixes = Object.freeze(['jit', '.well-known']);
|
||||||
|
const exceptionPrefixes = Object.freeze([
|
||||||
|
'/interaction',
|
||||||
|
'/experience',
|
||||||
|
'/sign-in-exp/default/check-password',
|
||||||
|
]);
|
||||||
|
|
||||||
const isPathParameter = (segment?: string) =>
|
const isPathParameter = (segment?: string) =>
|
||||||
Boolean(segment && (segment.startsWith(':') || segment.startsWith('{')));
|
Boolean(segment && (segment.startsWith(':') || segment.startsWith('{')));
|
||||||
|
@ -154,7 +159,7 @@ const throwIfNeeded = (method: OpenAPIV3.HttpMethods, path: string) => {
|
||||||
* @see {@link methodToVerb} for the mapping of HTTP methods to verbs.
|
* @see {@link methodToVerb} for the mapping of HTTP methods to verbs.
|
||||||
* @see {@link namespacePrefixes} for the list of namespace prefixes.
|
* @see {@link namespacePrefixes} for the list of namespace prefixes.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line complexity
|
|
||||||
export const buildOperationId = (method: OpenAPIV3.HttpMethods, path: string) => {
|
export const buildOperationId = (method: OpenAPIV3.HttpMethods, path: string) => {
|
||||||
const customOperationId = customRoutes[`${method} ${path}`];
|
const customOperationId = customRoutes[`${method} ${path}`];
|
||||||
|
|
||||||
|
@ -164,7 +169,7 @@ export const buildOperationId = (method: OpenAPIV3.HttpMethods, path: string) =>
|
||||||
|
|
||||||
// Skip interactions APIs as they are going to replaced by the new APIs soon.
|
// Skip interactions APIs as they are going to replaced by the new APIs soon.
|
||||||
// Skip experience APIs, as all the experience APIs' `operationId` will be customized in the custom openapi.json documents.
|
// Skip experience APIs, as all the experience APIs' `operationId` will be customized in the custom openapi.json documents.
|
||||||
if (path.startsWith('/interaction') || path.startsWith('/experience')) {
|
if (exceptionPrefixes.some((prefix) => path.startsWith(prefix))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
|
import { type PasswordPolicyChecker } from '@logto/core-kit';
|
||||||
|
import { type User, UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||||
|
import { condObject } from '@silverhand/essentials';
|
||||||
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';
|
||||||
|
@ -25,3 +27,19 @@ export const encryptPassword = async (
|
||||||
outputType: 'encoded',
|
outputType: 'encoded',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkPasswordPolicyForUser = async (
|
||||||
|
policyChecker: PasswordPolicyChecker,
|
||||||
|
password: string,
|
||||||
|
user: User
|
||||||
|
) => {
|
||||||
|
return policyChecker.check(
|
||||||
|
password,
|
||||||
|
condObject({
|
||||||
|
email: user.primaryEmail,
|
||||||
|
username: user.username,
|
||||||
|
phoneNumber: user.primaryPhone,
|
||||||
|
name: user.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -100,5 +100,5 @@ export const createUserWithAllRolesAndSignInToClient = async () => {
|
||||||
scopes: [PredefinedScope.All],
|
scopes: [PredefinedScope.All],
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id, client };
|
return { id, client, username, password };
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
resourceMe,
|
resourceMe,
|
||||||
} from '#src/helpers/admin-tenant.js';
|
} from '#src/helpers/admin-tenant.js';
|
||||||
import { expectRejects } from '#src/helpers/index.js';
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
|
import { generatePassword } from '#src/utils.js';
|
||||||
|
|
||||||
describe('me', () => {
|
describe('me', () => {
|
||||||
it('should only be available in admin tenant', async () => {
|
it('should only be available in admin tenant', async () => {
|
||||||
|
@ -59,4 +60,44 @@ describe('me', () => {
|
||||||
|
|
||||||
await deleteUser(id);
|
await deleteUser(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to verify and reset password', async () => {
|
||||||
|
const { id, client, password } = await createUserWithAllRolesAndSignInToClient();
|
||||||
|
const headers = { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` };
|
||||||
|
|
||||||
|
await expectRejects(
|
||||||
|
ky.post(logtoConsoleUrl + '/me/password/verify', {
|
||||||
|
headers,
|
||||||
|
json: { password: 'wrong-password' },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'session.invalid_credentials',
|
||||||
|
status: 422,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ky.post(logtoConsoleUrl + '/me/password/verify', {
|
||||||
|
headers,
|
||||||
|
json: { password },
|
||||||
|
})
|
||||||
|
).resolves.toHaveProperty('status', 204);
|
||||||
|
|
||||||
|
// Should reject weak password
|
||||||
|
await expect(
|
||||||
|
ky.post(logtoConsoleUrl + '/me/password', {
|
||||||
|
headers,
|
||||||
|
json: { password: '1' },
|
||||||
|
})
|
||||||
|
).rejects.toMatchInlineSnapshot('[HTTPError: Request failed with status code 400 Bad Request]');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ky.post(logtoConsoleUrl + '/me/password', {
|
||||||
|
headers,
|
||||||
|
json: { password: generatePassword() },
|
||||||
|
})
|
||||||
|
).resolves.toHaveProperty('status', 204);
|
||||||
|
|
||||||
|
await deleteUser(id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import { MfaPolicy, SignInIdentifier } from '@logto/schemas';
|
import { MfaPolicy, SignInIdentifier } from '@logto/schemas';
|
||||||
|
import { HTTPError, type ResponsePromise } from 'ky';
|
||||||
|
|
||||||
import { getSignInExperience, updateSignInExperience } from '#src/api/index.js';
|
import {
|
||||||
|
authedAdminApi,
|
||||||
|
createUser,
|
||||||
|
deleteUser,
|
||||||
|
getSignInExperience,
|
||||||
|
updateSignInExperience,
|
||||||
|
} from '#src/api/index.js';
|
||||||
import { expectRejects } from '#src/helpers/index.js';
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
|
import { generatePassword } from '#src/utils.js';
|
||||||
|
|
||||||
describe('admin console sign-in experience', () => {
|
describe('admin console sign-in experience', () => {
|
||||||
it('should get sign-in experience successfully', async () => {
|
it('should get sign-in experience successfully', async () => {
|
||||||
|
@ -49,3 +57,114 @@ describe('admin console sign-in experience', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expectPasswordIssues = async (promise: ResponsePromise, issueCodes: string[]) => {
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
fail('Should have thrown');
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof HTTPError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const body: { result: false; issues: Array<{ code: string }> } = await error.response.json();
|
||||||
|
expect(body.result).toEqual(false);
|
||||||
|
expect(body.issues.map((issue: { code: string }) => issue.code)).toEqual(issueCodes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('password policy', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await updateSignInExperience({
|
||||||
|
passwordPolicy: {
|
||||||
|
length: { min: 8, max: 64 },
|
||||||
|
characterTypes: { min: 3 },
|
||||||
|
rejects: {
|
||||||
|
pwned: true,
|
||||||
|
repetitionAndSequence: true,
|
||||||
|
userInfo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if rejects user info is enabled but no user id is provided', async () => {
|
||||||
|
try {
|
||||||
|
await authedAdminApi.post('sign-in-exp/default/check-password', {
|
||||||
|
json: {
|
||||||
|
password: 'johnny13',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fail('Should have thrown');
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof HTTPError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: unknown = await error.response.json();
|
||||||
|
|
||||||
|
console.log(body);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
code: 'request.invalid_input',
|
||||||
|
data: {
|
||||||
|
message: 'User information data is required to check user information.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw 400 for weak passwords', async () => {
|
||||||
|
const user = await createUser({ name: 'Johnny' });
|
||||||
|
await Promise.all([
|
||||||
|
expectPasswordIssues(
|
||||||
|
authedAdminApi.post('sign-in-exp/default/check-password', {
|
||||||
|
json: {
|
||||||
|
password: '123',
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
'password_rejected.too_short',
|
||||||
|
'password_rejected.character_types',
|
||||||
|
'password_rejected.restricted.sequence',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
expectPasswordIssues(
|
||||||
|
authedAdminApi.post('sign-in-exp/default/check-password', {
|
||||||
|
json: {
|
||||||
|
password: 'johnny13',
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
['password_rejected.character_types', 'password_rejected.restricted.user_info']
|
||||||
|
),
|
||||||
|
expectPasswordIssues(
|
||||||
|
authedAdminApi.post('sign-in-exp/default/check-password', {
|
||||||
|
json: {
|
||||||
|
password: '123456aA',
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
['password_rejected.pwned', 'password_rejected.restricted.sequence']
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await deleteUser(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept strong password', async () => {
|
||||||
|
const user = await createUser({ name: 'Johnny' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
authedAdminApi
|
||||||
|
.post('sign-in-exp/default/check-password', {
|
||||||
|
json: {
|
||||||
|
password: generatePassword(),
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json()
|
||||||
|
).resolves.toHaveProperty('result', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue