mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
Merge pull request #4454 from logto-io/gao-check-personal-info
refactor(core): check personal info when setting password
This commit is contained in:
commit
4675ad4eb4
3 changed files with 96 additions and 27 deletions
|
@ -72,13 +72,9 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
status: [204, 400, 401, 403, 422],
|
status: [204, 400, 401, 403, 422],
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(queries),
|
koaInteractionSie(queries),
|
||||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
|
||||||
await validatePassword(body.profile?.password, passwordPolicyChecker);
|
|
||||||
return next();
|
|
||||||
},
|
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { event, identifier, profile } = ctx.guard.body;
|
const { event, identifier, profile } = ctx.guard.body;
|
||||||
const { signInExperience, createLog } = ctx;
|
const { signInExperience, createLog, passwordPolicyChecker } = ctx;
|
||||||
|
|
||||||
const eventLog = createLog(`Interaction.${event}.Update`);
|
const eventLog = createLog(`Interaction.${event}.Update`);
|
||||||
eventLog.append({ event });
|
eventLog.append({ event });
|
||||||
|
@ -101,6 +97,11 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
|
|
||||||
eventLog.append({ profile, verifiedIdentifiers });
|
eventLog.append({ profile, verifiedIdentifiers });
|
||||||
|
|
||||||
|
await validatePassword(tenant, profile?.password, passwordPolicyChecker, {
|
||||||
|
identifiers: verifiedIdentifier,
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
|
||||||
await storeInteractionResult(
|
await storeInteractionResult(
|
||||||
{ event, identifiers: verifiedIdentifiers, profile },
|
{ event, identifiers: verifiedIdentifiers, profile },
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -210,17 +211,13 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
status: [204, 400, 404],
|
status: [204, 400, 404],
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(queries),
|
koaInteractionSie(queries),
|
||||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
|
||||||
await validatePassword(body.password, passwordPolicyChecker);
|
|
||||||
return next();
|
|
||||||
},
|
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const profilePayload = ctx.guard.body;
|
const profilePayload = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog, passwordPolicyChecker } = ctx;
|
||||||
|
|
||||||
// Check interaction exists
|
// Check interaction exists
|
||||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||||
const { event } = interactionStorage;
|
const { event, identifiers } = interactionStorage;
|
||||||
|
|
||||||
const profileLog = createLog(`Interaction.${event}.Profile.Create`);
|
const profileLog = createLog(`Interaction.${event}.Profile.Create`);
|
||||||
profileLog.append({ profile: profilePayload, interactionStorage });
|
profileLog.append({ profile: profilePayload, interactionStorage });
|
||||||
|
@ -229,6 +226,11 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
verifyProfileSettings(profilePayload, signInExperience);
|
verifyProfileSettings(profilePayload, signInExperience);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validatePassword(tenant, profilePayload.password, passwordPolicyChecker, {
|
||||||
|
identifiers,
|
||||||
|
profile: profilePayload,
|
||||||
|
});
|
||||||
|
|
||||||
await storeInteractionResult(
|
await storeInteractionResult(
|
||||||
{
|
{
|
||||||
profile: profilePayload,
|
profile: profilePayload,
|
||||||
|
@ -252,30 +254,31 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
status: [204, 400, 404],
|
status: [204, 400, 404],
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(queries),
|
koaInteractionSie(queries),
|
||||||
async ({ guard: { body }, passwordPolicyChecker }, next) => {
|
|
||||||
await validatePassword(body.password, passwordPolicyChecker);
|
|
||||||
return next();
|
|
||||||
},
|
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const profilePayload = ctx.guard.body;
|
const profilePayload = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog, passwordPolicyChecker } = ctx;
|
||||||
|
|
||||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||||
|
const { event, identifiers, profile } = interactionStorage;
|
||||||
|
const mergedProfile = { ...profile, ...profilePayload };
|
||||||
|
|
||||||
const profileLog = createLog(`Interaction.${interactionStorage.event}.Profile.Update`);
|
const profileLog = createLog(`Interaction.${event}.Profile.Update`);
|
||||||
|
|
||||||
profileLog.append({ profile: profilePayload, interactionStorage, method: 'PATCH' });
|
profileLog.append({ profile: profilePayload, interactionStorage, method: 'PATCH' });
|
||||||
|
|
||||||
if (interactionStorage.event !== InteractionEvent.ForgotPassword) {
|
if (event !== InteractionEvent.ForgotPassword) {
|
||||||
verifyProfileSettings(profilePayload, signInExperience);
|
verifyProfileSettings(profilePayload, signInExperience);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validatePassword(tenant, profilePayload.password, passwordPolicyChecker, {
|
||||||
|
identifiers,
|
||||||
|
// Merge with previous to provide a complete profile for validation
|
||||||
|
profile: mergedProfile,
|
||||||
|
});
|
||||||
|
|
||||||
await storeInteractionResult(
|
await storeInteractionResult(
|
||||||
{
|
{
|
||||||
profile: {
|
profile: mergedProfile,
|
||||||
...interactionStorage.profile,
|
|
||||||
...profilePayload,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
provider,
|
provider,
|
||||||
|
@ -386,9 +389,8 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
// Check interaction exists
|
// Check interaction exists
|
||||||
const { event } = getInteractionStorage(interactionDetails.result);
|
const { event } = getInteractionStorage(interactionDetails.result);
|
||||||
|
|
||||||
// This file needs refactor
|
|
||||||
// eslint-disable-next-line max-lines
|
|
||||||
await sendVerificationCodeToIdentifier(
|
await sendVerificationCodeToIdentifier(
|
||||||
|
// eslint-disable-next-line max-lines -- TODO: refactor @simeng
|
||||||
{ event, ...guard.body },
|
{ event, ...guard.body },
|
||||||
interactionDetails.jti,
|
interactionDetails.jti,
|
||||||
createLog,
|
createLog,
|
||||||
|
|
|
@ -2,6 +2,16 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
||||||
|
|
||||||
import type { UserIdentity } from '../types/index.js';
|
import type { UserIdentity } from '../types/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by the given identifier in the following order:
|
||||||
|
*
|
||||||
|
* 1. Find user by username
|
||||||
|
* 2. Find user by email
|
||||||
|
* 3. Find user by phone
|
||||||
|
* 4. Find user by social identity
|
||||||
|
*
|
||||||
|
* @returns The found user or `null` if no user is found.
|
||||||
|
*/
|
||||||
export default async function findUserByIdentifier(
|
export default async function findUserByIdentifier(
|
||||||
{ queries, connectors }: TenantContext,
|
{ queries, connectors }: TenantContext,
|
||||||
identity: UserIdentity
|
identity: UserIdentity
|
||||||
|
|
|
@ -1,21 +1,78 @@
|
||||||
import { type PasswordPolicyChecker } from '@logto/core-kit';
|
import { type PasswordPolicyChecker, type UserInfo } from '@logto/core-kit';
|
||||||
import { type Optional } from '@silverhand/essentials';
|
import { type Optional } from '@silverhand/essentials';
|
||||||
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||||
|
|
||||||
|
import { type AnonymousInteractionResult } from '../types/index.js';
|
||||||
|
|
||||||
|
import findUserByIdentifier from './find-user-by-identifier.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch current user information from the interaction storage by checking `identifiers`
|
||||||
|
* and `profile` properties.
|
||||||
|
*
|
||||||
|
* Data in `profile` will override data in `identifiers`, if any. Because `profile` will
|
||||||
|
* overwrite the data in `identifiers` if this interaction is validated.
|
||||||
|
*/
|
||||||
|
const fetchUserInfo = async (
|
||||||
|
tenant: TenantContext,
|
||||||
|
{ identifiers, profile }: Pick<AnonymousInteractionResult, 'identifiers' | 'profile'>
|
||||||
|
): Promise<UserInfo> => {
|
||||||
|
const users = await Promise.all(
|
||||||
|
identifiers?.map(async (identifier) => {
|
||||||
|
switch (identifier.key) {
|
||||||
|
case 'emailVerified': {
|
||||||
|
return findUserByIdentifier(tenant, { email: identifier.value });
|
||||||
|
}
|
||||||
|
case 'phoneVerified': {
|
||||||
|
return findUserByIdentifier(tenant, { phone: identifier.value });
|
||||||
|
}
|
||||||
|
case 'social': {
|
||||||
|
return findUserByIdentifier(tenant, identifier);
|
||||||
|
}
|
||||||
|
case 'accountId': {
|
||||||
|
return tenant.queries.users.findUserById(identifier.value);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the first non-null user as the current user. Users with different account IDs
|
||||||
|
// should not be mixed in the same interaction, and it should be validated in some other
|
||||||
|
// places.
|
||||||
|
const user = users.find((user) => user !== null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: user?.username ?? profile?.username,
|
||||||
|
email: user?.primaryEmail ?? profile?.email,
|
||||||
|
phoneNumber: user?.primaryPhone ?? profile?.phone,
|
||||||
|
name: user?.name ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate password against the given password policy if the password is not undefined,
|
* Validate password against the given password policy if the password is not undefined,
|
||||||
* throw a {@link RequestError} if the password is invalid; otherwise, do nothing.
|
* throw a {@link RequestError} if the password is invalid; otherwise, do nothing.
|
||||||
*/
|
*/
|
||||||
export const validatePassword = async (
|
export const validatePassword = async (
|
||||||
|
tenant: TenantContext,
|
||||||
password: Optional<string>,
|
password: Optional<string>,
|
||||||
checker: PasswordPolicyChecker
|
checker: PasswordPolicyChecker,
|
||||||
|
{ identifiers, profile }: Pick<AnonymousInteractionResult, 'identifiers' | 'profile'>
|
||||||
) => {
|
) => {
|
||||||
if (password === undefined) {
|
if (password === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issues = await checker.check(password, {});
|
const issues = await checker.check(
|
||||||
|
password,
|
||||||
|
checker.policy.rejects.userInfo ? await fetchUserInfo(tenant, { identifiers, profile }) : {}
|
||||||
|
);
|
||||||
|
|
||||||
if (issues.length > 0) {
|
if (issues.length > 0) {
|
||||||
throw new RequestError('password.rejected', issues);
|
throw new RequestError('password.rejected', issues);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue