diff --git a/packages/core/src/libraries/hook.test.ts b/packages/core/src/libraries/hook.test.ts index 09a4a83af..43ea97df7 100644 --- a/packages/core/src/libraries/hook.test.ts +++ b/packages/core/src/libraries/hook.test.ts @@ -34,9 +34,7 @@ mockEsm('#src/queries/user.js', () => ({ mockEsm('#src/queries/application.js', () => ({ findApplicationById: () => ({ id: 'app_id', extraField: 'not_ok' }), })); -mockEsm('#src/connectors/index.js', () => ({ - getLogtoConnectorById: () => ({ metadata: { id: 'connector_id', extraField: 'not_ok' } }), -})); + // eslint-disable-next-line unicorn/consistent-function-scoping mockEsmDefault('#src/env-set/create-query-client-by-env.js', () => () => queryClient); jest.spyOn(queryClient, 'query').mockImplementation(queryFunction); @@ -49,7 +47,7 @@ describe('triggerInteractionHooksIfNeeded()', () => { }); it('should return if no user ID found', async () => { - await triggerInteractionHooksIfNeeded({ event: Event.SignIn }); + await triggerInteractionHooksIfNeeded(); expect(queryFunction).not.toBeCalled(); }); @@ -58,11 +56,14 @@ describe('triggerInteractionHooksIfNeeded()', () => { jest.useFakeTimers().setSystemTime(100_000); await triggerInteractionHooksIfNeeded( - { event: Event.SignIn, identifier: { connectorId: 'bar' } }, // @ts-expect-error for testing { jti: 'some_jti', - result: { login: { accountId: '123' } }, + result: { + login: { accountId: '123' }, + event: Event.SignIn, + identifier: { connectorId: 'bar' }, + }, params: { client_id: 'some_client' }, } as Interaction ); @@ -78,7 +79,6 @@ describe('triggerInteractionHooksIfNeeded()', () => { userId: '123', user: { id: 'user_id', username: 'user' }, application: { id: 'app_id' }, - connectors: [{ id: 'connector_id' }], createdAt: new Date(100_000).toISOString(), }, retry: { limit: 3 }, diff --git a/packages/core/src/libraries/hook.ts b/packages/core/src/libraries/hook.ts index 3c8082b9d..6f1b5fd7b 100644 --- a/packages/core/src/libraries/hook.ts +++ b/packages/core/src/libraries/hook.ts @@ -5,11 +5,10 @@ import { conditional } from '@silverhand/essentials'; import { got } from 'got'; import type { Provider } from 'oidc-provider'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import modelRouters from '#src/model-routers/index.js'; import { findApplicationById } from '#src/queries/application.js'; import { findUserById } from '#src/queries/user.js'; -import type { InteractionPayload } from '#src/routes/interaction/types/index.js'; +import { getInteractionStorage } from '#src/routes/interaction/utils/interaction.js'; const eventToHook: Record = { [Event.Register]: HookEvent.PostRegister, @@ -31,7 +30,6 @@ const pick = >( export type Interaction = Awaited>; export const triggerInteractionHooksIfNeeded = async ( - interactionPayload: InteractionPayload, details?: Interaction, userAgent?: string ) => { @@ -43,22 +41,19 @@ export const triggerInteractionHooksIfNeeded = async ( return; } - const { event, identifier } = interactionPayload; + const interactionPayload = getInteractionStorage(details.result); + const { event } = interactionPayload; + const hookEvent = eventToHook[event]; const { rows } = await modelRouters.hook.client.readAll(); - const [user, application, connector] = await Promise.all([ + + const [user, application] = await Promise.all([ trySafe(findUserById(userId)), trySafe(async () => conditional(typeof applicationId === 'string' && (await findApplicationById(applicationId))) ), - trySafe(async () => - conditional( - identifier && - 'connectorId' in identifier && - (await getLogtoConnectorById(identifier.connectorId)) - ) - ), ]); + const payload = { event: hookEvent, interactionEvent: event, @@ -68,9 +63,6 @@ export const triggerInteractionHooksIfNeeded = async ( userId, user: user && pick(user, ...userInfoSelectFields), application: application && pick(application, 'id', 'type', 'name', 'description'), - connectors: connector && [ - pick(connector.metadata, 'id', 'name', 'description', 'platform', 'target', 'isStandard'), - ], } satisfies Omit; await Promise.all( diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index eb66c2cb3..8f33ff8d5 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -45,21 +45,22 @@ await mockEsmWithActual('#src/connectors/index.js', () => ({ }), })); +await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({ + getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), +})); + const { assignInteractionResults } = await mockEsmWithActual('#src/libraries/session.js', () => ({ assignInteractionResults: jest.fn(), })); -const { - getSignInExperience, - verifySignInModeSettings, - verifyIdentifierSettings, - verifyProfileSettings, -} = mockEsm('./utils/sign-in-experience-validation.js', () => ({ - getSignInExperience: jest.fn(async () => mockSignInExperience), - verifySignInModeSettings: jest.fn(), - verifyIdentifierSettings: jest.fn(), - verifyProfileSettings: jest.fn(), -})); +const { verifySignInModeSettings, verifyIdentifierSettings, verifyProfileSettings } = mockEsm( + './utils/sign-in-experience-validation.js', + () => ({ + verifySignInModeSettings: jest.fn(), + verifyIdentifierSettings: jest.fn(), + verifyProfileSettings: jest.fn(), + }) +); const submitInteraction = mockEsmDefault('./actions/submit-interaction.js', () => jest.fn()); @@ -133,7 +134,6 @@ describe('session -> interactionRoutes', () => { profile: { phone: '1234567890' }, }; const response = await sessionRequest.put(path).send(body); - expect(getSignInExperience).toBeCalled(); expect(verifySignInModeSettings).toBeCalled(); expect(verifyIdentifierSettings).toBeCalled(); expect(verifyProfileSettings).toBeCalled(); @@ -154,7 +154,7 @@ describe('session -> interactionRoutes', () => { const path = `${interactionPrefix}/event`; it('should call verifySignInModeSettings properly', async () => { - getInteractionStorage.mockResolvedValueOnce({ + getInteractionStorage.mockReturnValueOnce({ event: Event.SignIn, }); const body = { @@ -169,7 +169,7 @@ describe('session -> interactionRoutes', () => { }); it('should reject if switch sign-in event to forgot-password directly', async () => { - getInteractionStorage.mockResolvedValueOnce({ + getInteractionStorage.mockReturnValueOnce({ event: Event.SignIn, }); @@ -184,7 +184,7 @@ describe('session -> interactionRoutes', () => { }); it('should reject if switch forgot-password to sign-in directly', async () => { - getInteractionStorage.mockResolvedValueOnce({ + getInteractionStorage.mockReturnValueOnce({ event: Event.ForgotPassword, }); @@ -272,7 +272,7 @@ describe('session -> interactionRoutes', () => { }); it('should not call validateMandatoryUserProfile for forgot password request', async () => { - getInteractionStorage.mockResolvedValueOnce({ + getInteractionStorage.mockReturnValueOnce({ event: Event.ForgotPassword, }); diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index decb86f58..92170e8b9 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -1,6 +1,6 @@ import type { LogtoErrorCode } from '@logto/phrases'; import { Event, eventGuard, identifierPayloadGuard, profileGuard } from '@logto/schemas'; -import { conditional } from '@silverhand/essentials'; +import type Router from 'koa-router'; import type { Provider } from 'oidc-provider'; import { z } from 'zod'; @@ -12,6 +12,10 @@ import assertThat from '#src/utils/assert-that.js'; import type { AnonymousRouter } from '../types.js'; import submitInteraction from './actions/submit-interaction.js'; +import koaInteractionDetails from './middleware/koa-interaction-details.js'; +import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js'; +import koaInteractionHooks from './middleware/koa-interaction-hooks.js'; +import koaInteractionSIE from './middleware/koa-interaction-sie.js'; import { sendPasscodePayloadGuard, socialAuthorizationUrlPayloadGuard } from './types/guard.js'; import { getInteractionStorage, @@ -20,7 +24,6 @@ import { } from './utils/interaction.js'; import { sendPasscodeToIdentifier } from './utils/passcode-validation.js'; import { - getSignInExperience, verifySignInModeSettings, verifyIdentifierSettings, verifyProfileSettings, @@ -36,27 +39,19 @@ import { export const interactionPrefix = '/interaction'; export const verificationPath = 'verification'; +type RouterContext = T extends Router ? Context : never; + export default function interactionRoutes( - router: T, + anonymousRouter: T, provider: Provider ) { - router.use(koaAuditLog(), async (ctx, next) => { - await next(); - - // Prepend interaction context to log entries - try { - const { - jti, - params: { client_id }, - } = await provider.interactionDetails(ctx.req, ctx.res); - ctx.prependAllLogEntries({ - sessionId: jti, - applicationId: conditional(typeof client_id === 'string' && client_id), - }); - } catch (error: unknown) { - console.error(`Failed to get oidc provider interaction details`, error); - } - }); + const router = + // @ts-expect-error for good koa types + // eslint-disable-next-line no-restricted-syntax + (anonymousRouter as Router>>).use( + koaAuditLog(), + koaInteractionDetails(provider) + ); // Create a new interaction router.put( @@ -68,18 +63,19 @@ export default function interactionRoutes( profile: profileGuard.optional(), }), }), + koaInteractionSIE(), async (ctx, next) => { const { event, identifier, profile } = ctx.guard.body; - const experience = await getSignInExperience(ctx, provider); + const { signInExperience } = ctx; - verifySignInModeSettings(event, experience); + verifySignInModeSettings(event, signInExperience); if (identifier) { - verifyIdentifierSettings(identifier, experience); + verifyIdentifierSettings(identifier, signInExperience); } if (profile) { - verifyProfileSettings(profile, experience); + verifyProfileSettings(profile, signInExperience); } const verifiedIdentifier = identifier && [ @@ -102,7 +98,6 @@ export default function interactionRoutes( // Delete Interaction router.delete(interactionPrefix, async (ctx, next) => { - await provider.interactionDetails(ctx.req, ctx.res); const error: LogtoErrorCode = 'oidc.aborted'; await assignInteractionResults(ctx, provider, { error }); @@ -113,11 +108,14 @@ export default function interactionRoutes( router.put( `${interactionPrefix}/event`, koaGuard({ body: z.object({ event: eventGuard }) }), + koaInteractionSIE(), async (ctx, next) => { const { event } = ctx.guard.body; - verifySignInModeSettings(event, await getSignInExperience(ctx, provider)); + const { signInExperience, interactionDetails } = ctx; - const interactionStorage = await getInteractionStorage(ctx, provider); + verifySignInModeSettings(event, signInExperience); + + const interactionStorage = getInteractionStorage(interactionDetails.result); // Forgot Password specific event interaction storage can't be shared with other types of interactions assertThat( @@ -143,11 +141,13 @@ export default function interactionRoutes( koaGuard({ body: identifierPayloadGuard, }), + koaInteractionSIE(), async (ctx, next) => { const identifierPayload = ctx.guard.body; - verifyIdentifierSettings(identifierPayload, await getSignInExperience(ctx, provider)); + const { signInExperience, interactionDetails } = ctx; + verifyIdentifierSettings(identifierPayload, signInExperience); - const interactionStorage = await getInteractionStorage(ctx, provider); + const interactionStorage = getInteractionStorage(interactionDetails.result); const verifiedIdentifier = await verifyIdentifierPayload( ctx, @@ -172,11 +172,13 @@ export default function interactionRoutes( koaGuard({ body: profileGuard, }), + koaInteractionSIE(), async (ctx, next) => { const profilePayload = ctx.guard.body; - verifyProfileSettings(profilePayload, await getSignInExperience(ctx, provider)); + const { signInExperience, interactionDetails } = ctx; + verifyProfileSettings(profilePayload, signInExperience); - const interactionStorage = await getInteractionStorage(ctx, provider); + const interactionStorage = getInteractionStorage(interactionDetails.result); await storeInteractionResult( { @@ -198,7 +200,8 @@ export default function interactionRoutes( // Delete Interaction Profile router.delete(`${interactionPrefix}/profile`, async (ctx, next) => { - const interactionStorage = await getInteractionStorage(ctx, provider); + const { interactionDetails } = ctx; + const interactionStorage = getInteractionStorage(interactionDetails.result); const { profile, ...rest } = interactionStorage; await storeInteractionResult(rest, ctx, provider); @@ -208,23 +211,29 @@ export default function interactionRoutes( }); // Submit Interaction - router.post(`${interactionPrefix}/submit`, async (ctx, next) => { - const interactionStorage = await getInteractionStorage(ctx, provider); + router.post( + `${interactionPrefix}/submit`, + koaInteractionSIE(), + koaInteractionHooks(), + async (ctx, next) => { + const { interactionDetails } = ctx; + const interactionStorage = getInteractionStorage(interactionDetails.result); - const { event } = interactionStorage; + const { event } = interactionStorage; - const accountVerifiedInteraction = await verifyIdentifier(ctx, provider, interactionStorage); + const accountVerifiedInteraction = await verifyIdentifier(ctx, provider, interactionStorage); - const verifiedInteraction = await verifyProfile(accountVerifiedInteraction); + const verifiedInteraction = await verifyProfile(accountVerifiedInteraction); - if (event !== Event.ForgotPassword) { - await validateMandatoryUserProfile(ctx, provider, verifiedInteraction); + if (event !== Event.ForgotPassword) { + await validateMandatoryUserProfile(ctx, verifiedInteraction); + } + + await submitInteraction(verifiedInteraction, ctx, provider); + + return next(); } - - await submitInteraction(verifiedInteraction, ctx, provider); - - return next(); - }); + ); // Create social authorization url interaction verification router.post( @@ -232,7 +241,7 @@ export default function interactionRoutes( koaGuard({ body: socialAuthorizationUrlPayloadGuard }), async (ctx, next) => { // Check interaction exists - await getInteractionStorage(ctx, provider); + getInteractionStorage(ctx.interactionDetails.result); const { body: payload } = ctx.guard; @@ -252,7 +261,7 @@ export default function interactionRoutes( }), async (ctx, next) => { // Check interaction exists - await getInteractionStorage(ctx, provider); + getInteractionStorage(ctx.interactionDetails.result); const { jti } = await provider.interactionDetails(ctx.req, ctx.res); await sendPasscodeToIdentifier(ctx.guard.body, jti, ctx.createLog); diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-details.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-details.ts index 514063c5f..6d90cc226 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-details.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-details.ts @@ -1,13 +1,15 @@ import type { MiddlewareType } from 'koa'; import type { Provider } from 'oidc-provider'; -export type WithInteractionDetailsContext = ContextT & { +import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; + +export type WithInteractionDetailsContext = ContextT & { interactionDetails: Awaited>; }; -export default function koaInteractionDetails( +export default function koaInteractionDetails( provider: Provider -): MiddlewareType, ResponseBodyT> { +): MiddlewareType> { return async (ctx, next) => { ctx.interactionDetails = await provider.interactionDetails(ctx.req, ctx.res); diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-hooks.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-hooks.ts index b8333917c..4cac93580 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-hooks.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-hooks.ts @@ -3,23 +3,16 @@ import type { IRouterParamContext } from 'koa-router'; import { triggerInteractionHooksIfNeeded } from '#src/libraries/hook.js'; -import type { WithInteractionPayloadContext } from './koa-interaction-body-guard.js'; import type { WithInteractionDetailsContext } from './koa-interaction-details.js'; export default function koaInteractionHooks< StateT, - ContextT extends WithInteractionPayloadContext< - WithInteractionDetailsContext - >, + ContextT extends WithInteractionDetailsContext, ResponseT >(): MiddlewareType { return async (ctx, next) => { await next(); - void triggerInteractionHooksIfNeeded( - ctx.interactionPayload, - ctx.interactionDetails, - ctx.header['user-agent'] - ); + void triggerInteractionHooksIfNeeded(ctx.interactionDetails, ctx.header['user-agent']); }; } diff --git a/packages/core/src/routes/interaction/utils/interaction.ts b/packages/core/src/routes/interaction/utils/interaction.ts index fa46d85ea..0fca4c13e 100644 --- a/packages/core/src/routes/interaction/utils/interaction.ts +++ b/packages/core/src/routes/interaction/utils/interaction.ts @@ -2,7 +2,7 @@ import type { ConnectorSession } from '@logto/connector-kit'; import { connectorSessionGuard } from '@logto/connector-kit'; import type { Event, Profile } from '@logto/schemas'; import type { Context } from 'koa'; -import type { Provider } from 'oidc-provider'; +import type { Provider, InteractionResults } from 'oidc-provider'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -83,10 +83,6 @@ export const isAccountVerifiedInteractionResult = ( interaction: AnonymousInteractionResult ): interaction is AccountVerifiedInteractionResult => Boolean(interaction.accountId); -type Options = { - merge?: boolean; -}; - export const storeInteractionResult = async ( interaction: Omit & { event?: Event }, ctx: Context, @@ -107,12 +103,10 @@ export const storeInteractionResult = async ( ); }; -export const getInteractionStorage = async ( - ctx: Context, - provider: Provider -): Promise => { - const { result } = await provider.interactionDetails(ctx.req, ctx.res); - const parseResult = anonymousInteractionResultGuard.safeParse(result); +export const getInteractionStorage = ( + interaction?: InteractionResults +): AnonymousInteractionResult => { + const parseResult = anonymousInteractionResultGuard.safeParse(interaction); assertThat( parseResult.success, diff --git a/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts b/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts index ef57d192e..b3e914405 100644 --- a/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts +++ b/packages/core/src/routes/interaction/utils/sign-in-experience-validation.ts @@ -1,10 +1,7 @@ import type { SignInExperience, Profile, IdentifierPayload } from '@logto/schemas'; import { SignInMode, SignInIdentifier, Event } from '@logto/schemas'; -import type { Context } from 'koa'; -import type { Provider } from 'oidc-provider'; import RequestError from '#src/errors/RequestError/index.js'; -import { getSignInExperienceForApplication } from '#src/libraries/sign-in-experience/index.js'; import assertThat from '#src/utils/assert-that.js'; const forbiddenEventError = new RequestError({ code: 'auth.forbidden', status: 403 }); @@ -122,11 +119,3 @@ export const verifyProfileSettings = (profile: Profile, { signUp }: SignInExperi assertThat(signUp.password, forbiddenIdentifierError); } }; - -export const getSignInExperience = async (ctx: Context, provider: Provider) => { - const interaction = await provider.interactionDetails(ctx.req, ctx.res); - - return getSignInExperienceForApplication( - typeof interaction.params.client_id === 'string' ? interaction.params.client_id : undefined - ); -}; diff --git a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts index 4951d691d..b2bbc38b3 100644 --- a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts +++ b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts @@ -1,5 +1,6 @@ import { Event, MissingProfile, SignInIdentifier } from '@logto/schemas'; import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; +import type { Provider } from 'oidc-provider'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import RequestError from '#src/errors/RequestError/index.js'; @@ -18,24 +19,25 @@ const { isUserPasswordSet } = mockEsm('../utils/index.js', () => ({ isUserPasswordSet: jest.fn(), })); -const { getSignInExperience } = mockEsm('../utils/sign-in-experience-validation.js', () => ({ - getSignInExperience: jest.fn().mockReturnValue(mockSignInExperience), -})); - const validateMandatoryUserProfile = await pickDefault( import('./mandatory-user-profile-validation.js') ); describe('validateMandatoryUserProfile', () => { const provider = createMockProvider(); - const baseCtx = createContextWithRouteParameters(); + const baseCtx = { + ...createContextWithRouteParameters(), + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + interactionDetails: {} as Awaited>, + signInExperience: mockSignInExperience, + }; const interaction: IdentifierVerifiedInteractionResult = { event: Event.SignIn, accountId: 'foo', }; it('username and password missing but required', async () => { - await expect(validateMandatoryUserProfile(baseCtx, provider, interaction)).rejects.toMatchError( + await expect(validateMandatoryUserProfile(baseCtx, interaction)).rejects.toMatchError( new RequestError( { code: 'user.missing_profile', status: 422 }, { missingProfile: [MissingProfile.password, MissingProfile.username] } @@ -43,7 +45,7 @@ describe('validateMandatoryUserProfile', () => { ); await expect( - validateMandatoryUserProfile(baseCtx, provider, { + validateMandatoryUserProfile(baseCtx, { ...interaction, profile: { username: 'username', @@ -59,18 +61,19 @@ describe('validateMandatoryUserProfile', () => { }); isUserPasswordSet.mockResolvedValueOnce(true); - await expect( - validateMandatoryUserProfile(baseCtx, provider, interaction) - ).resolves.not.toThrow(); + await expect(validateMandatoryUserProfile(baseCtx, interaction)).resolves.not.toThrow(); }); it('email missing but required', async () => { - getSignInExperience.mockResolvedValueOnce({ - ...mockSignInExperience, - signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true }, - }); + const ctx = { + ...baseCtx, + signInExperience: { + ...mockSignInExperience, + signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true }, + }, + }; - await expect(validateMandatoryUserProfile(baseCtx, provider, interaction)).rejects.toMatchError( + await expect(validateMandatoryUserProfile(ctx, interaction)).rejects.toMatchError( new RequestError( { code: 'user.missing_profile', status: 422 }, { missingProfile: [MissingProfile.email] } @@ -83,23 +86,27 @@ describe('validateMandatoryUserProfile', () => { primaryEmail: 'email', }); - getSignInExperience.mockResolvedValueOnce({ - ...mockSignInExperience, - signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true }, - }); + const ctx = { + ...baseCtx, + signInExperience: { + ...mockSignInExperience, + signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true }, + }, + }; - await expect( - validateMandatoryUserProfile(baseCtx, provider, interaction) - ).resolves.not.toThrow(); + await expect(validateMandatoryUserProfile(ctx, interaction)).resolves.not.toThrow(); }); it('phone missing but required', async () => { - getSignInExperience.mockResolvedValueOnce({ - ...mockSignInExperience, - signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true }, - }); + const ctx = { + ...baseCtx, + signInExperience: { + ...mockSignInExperience, + signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true }, + }, + }; - await expect(validateMandatoryUserProfile(baseCtx, provider, interaction)).rejects.toMatchError( + await expect(validateMandatoryUserProfile(ctx, interaction)).rejects.toMatchError( new RequestError( { code: 'user.missing_profile', status: 422 }, { missingProfile: [MissingProfile.phone] } @@ -112,27 +119,31 @@ describe('validateMandatoryUserProfile', () => { primaryPhone: 'phone', }); - getSignInExperience.mockResolvedValueOnce({ - ...mockSignInExperience, - signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true }, - }); + const ctx = { + ...baseCtx, + signInExperience: { + ...mockSignInExperience, + signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true }, + }, + }; - await expect( - validateMandatoryUserProfile(baseCtx, provider, interaction) - ).resolves.not.toThrow(); + await expect(validateMandatoryUserProfile(ctx, interaction)).resolves.not.toThrow(); }); it('email or Phone required', async () => { - getSignInExperience.mockResolvedValue({ - ...mockSignInExperience, - signUp: { - identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms], - password: false, - verify: true, + const ctx = { + ...baseCtx, + signInExperience: { + ...mockSignInExperience, + signUp: { + identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms], + password: false, + verify: true, + }, }, - }); + }; - await expect(validateMandatoryUserProfile(baseCtx, provider, interaction)).rejects.toMatchError( + await expect(validateMandatoryUserProfile(ctx, interaction)).rejects.toMatchError( new RequestError( { code: 'user.missing_profile', status: 422 }, { missingProfile: [MissingProfile.emailOrPhone] } @@ -140,14 +151,14 @@ describe('validateMandatoryUserProfile', () => { ); await expect( - validateMandatoryUserProfile(baseCtx, provider, { + validateMandatoryUserProfile(ctx, { ...interaction, profile: { email: 'email' }, }) ).resolves.not.toThrow(); await expect( - validateMandatoryUserProfile(baseCtx, provider, { + validateMandatoryUserProfile(ctx, { ...interaction, profile: { phone: '123456' }, }) diff --git a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts index 121d086a1..b5db2786d 100644 --- a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts +++ b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts @@ -2,15 +2,14 @@ import type { Profile, SignInExperience, User } from '@logto/schemas'; import { Event, MissingProfile, SignInIdentifier } from '@logto/schemas'; import type { Nullable } from '@silverhand/essentials'; import type { Context } from 'koa'; -import type { Provider } from 'oidc-provider'; import RequestError from '#src/errors/RequestError/index.js'; import { findUserById } from '#src/queries/user.js'; import assertThat from '#src/utils/assert-that.js'; +import type { WithInteractionSIEContext } from '../middleware/koa-interaction-sie.js'; import type { IdentifierVerifiedInteractionResult } from '../types/index.js'; import { isUserPasswordSet } from '../utils/index.js'; -import { getSignInExperience } from '../utils/sign-in-experience-validation.js'; // eslint-disable-next-line complexity const getMissingProfileBySignUpIdentifiers = ({ @@ -71,11 +70,10 @@ const getMissingProfileBySignUpIdentifiers = ({ }; export default async function validateMandatoryUserProfile( - ctx: Context, - provider: Provider, + ctx: WithInteractionSIEContext, interaction: IdentifierVerifiedInteractionResult ) { - const { signUp } = await getSignInExperience(ctx, provider); + const { signUp } = ctx.signInExperience; const { event, accountId, profile } = interaction; const user = event === Event.Register ? null : await findUserById(accountId);