mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(core): add webhooks middleware to experience api (#6357)
* refactor(core): refactor backup code generate flow refactor backup code generate flow * fix(core): fix api payload fix api payload * fix(core): fix rebase issue fix rebase issue * feat(core): add hooks middleware to experience APIs add interaction hooks to experience APIs * refactor(core): refactor experience API context type refactor experience API context type
This commit is contained in:
parent
dbc5512c0b
commit
3b4da16247
30 changed files with 574 additions and 106 deletions
|
@ -17,6 +17,8 @@ import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
|||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import { type WithHooksAndLogsContext } from '../types.js';
|
||||
|
||||
import { EmailCodeVerification } from './verifications/code-verification.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
@ -36,7 +38,7 @@ const userQueries = {
|
|||
const userLibraries = {
|
||||
generateUserId: jest.fn().mockResolvedValue('uid'),
|
||||
insertUser: jest.fn(async (user: CreateUser): Promise<InsertUserResult> => [user as User]),
|
||||
provisionOrganizations: jest.fn(),
|
||||
provisionOrganizations: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const ssoConnectors = {
|
||||
getAvailableSsoConnectors: jest.fn().mockResolvedValue([]),
|
||||
|
@ -69,7 +71,11 @@ describe('ExperienceInteraction class', () => {
|
|||
undefined,
|
||||
{ users: userLibraries, ssoConnectors }
|
||||
);
|
||||
const ctx = {
|
||||
|
||||
// @ts-expect-error --mock test context
|
||||
const ctx: WithHooksAndLogsContext = {
|
||||
assignInteractionHookResult: jest.fn(),
|
||||
appendDataHookContext: jest.fn(),
|
||||
...createContextWithRouteParameters(),
|
||||
...createMockLogContext(),
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ import { conditional } from '@silverhand/essentials';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
type Interaction,
|
||||
type InteractionContext,
|
||||
type InteractionProfile,
|
||||
type WithHooksAndLogsContext,
|
||||
} from '../types.js';
|
||||
|
||||
import {
|
||||
|
@ -77,13 +77,17 @@ export default class ExperienceInteraction {
|
|||
/**
|
||||
* Restore experience interaction from the interaction storage.
|
||||
*/
|
||||
constructor(ctx: WithLogContext, tenant: TenantContext, interactionDetails: Interaction);
|
||||
constructor(ctx: WithHooksAndLogsContext, tenant: TenantContext, interactionDetails: Interaction);
|
||||
/**
|
||||
* Create a new `ExperienceInteraction` instance.
|
||||
*/
|
||||
constructor(ctx: WithLogContext, tenant: TenantContext, interactionEvent: InteractionEvent);
|
||||
constructor(
|
||||
private readonly ctx: WithLogContext,
|
||||
ctx: WithHooksAndLogsContext,
|
||||
tenant: TenantContext,
|
||||
interactionEvent: InteractionEvent
|
||||
);
|
||||
constructor(
|
||||
private readonly ctx: WithHooksAndLogsContext,
|
||||
private readonly tenant: TenantContext,
|
||||
interactionData: Interaction | InteractionEvent
|
||||
) {
|
||||
|
@ -302,14 +306,15 @@ export default class ExperienceInteraction {
|
|||
new RequestError({ code: 'user.new_password_required_in_profile', status: 422 })
|
||||
);
|
||||
|
||||
await userQueries.updateUserById(user.id, {
|
||||
const updatedUser = await userQueries.updateUserById(user.id, {
|
||||
passwordEncrypted,
|
||||
passwordEncryptionMethod,
|
||||
});
|
||||
|
||||
await this.cleanUp();
|
||||
|
||||
// TODO: User data updated hook
|
||||
this.ctx.assignInteractionHookResult({ userId: user.id });
|
||||
this.ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -333,7 +338,7 @@ export default class ExperienceInteraction {
|
|||
const { mfaSkipped, mfaVerifications } = this.mfa.toUserMfaVerifications();
|
||||
|
||||
// Update user profile
|
||||
await userQueries.updateUserById(user.id, {
|
||||
const updatedUser = await userQueries.updateUserById(user.id, {
|
||||
...rest,
|
||||
...conditional(
|
||||
socialIdentity && {
|
||||
|
@ -371,9 +376,13 @@ export default class ExperienceInteraction {
|
|||
login: { accountId: user.id },
|
||||
});
|
||||
|
||||
// TODO: PostInteractionHooks
|
||||
|
||||
this.ctx.body = { redirectTo };
|
||||
|
||||
this.ctx.assignInteractionHookResult({ userId: user.id });
|
||||
|
||||
if (Object.keys(this.profile.data).length > 0 || mfaVerifications.length > 0) {
|
||||
this.ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert the current interaction to JSON, so that it can be stored as the OIDC provider interaction result */
|
||||
|
|
|
@ -19,13 +19,12 @@ import { generateStandardId } from '@logto/shared';
|
|||
import { conditional, conditionalArray, trySafe } from '@silverhand/essentials';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||
import { buildAppInsightsTelemetry } from '#src/utils/request.js';
|
||||
import { getTenantId } from '#src/utils/tenant.js';
|
||||
|
||||
import { type InteractionProfile } from '../../types.js';
|
||||
import { type InteractionProfile, type WithHooksAndLogsContext } from '../../types.js';
|
||||
import { postAffiliateLogs } from '../helpers.js';
|
||||
import { toUserSocialIdentityData } from '../utils.js';
|
||||
|
||||
|
@ -42,7 +41,7 @@ type OrganizationProvisionPayload =
|
|||
export class ProvisionLibrary {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContext,
|
||||
private readonly ctx: WithLogContext
|
||||
private readonly ctx: WithHooksAndLogsContext
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
@ -88,7 +87,7 @@ export class ProvisionLibrary {
|
|||
|
||||
await this.provisionNewUserJitOrganizations(user.id, profile);
|
||||
|
||||
// TODO: New user created hooks
|
||||
this.ctx.appendDataHookContext('User.Created', { user });
|
||||
// TODO: log
|
||||
|
||||
this.triggerAnalyticReports(user);
|
||||
|
@ -255,7 +254,11 @@ export class ProvisionLibrary {
|
|||
|
||||
const provisionedOrganizations = await usersLibraries.provisionOrganizations(payload);
|
||||
|
||||
// TODO: trigger hooks event
|
||||
for (const { organizationId } of provisionedOrganizations) {
|
||||
this.ctx.appendDataHookContext('Organization.Membership.Updated', {
|
||||
organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
return provisionedOrganizations;
|
||||
}
|
||||
|
|
|
@ -14,17 +14,17 @@ import { identificationApiPayloadGuard, InteractionEvent } from '@logto/schemas'
|
|||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import koaAuditLog from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaInteractionDetails from '#src/middleware/koa-interaction-details.js';
|
||||
|
||||
import { type AnonymousRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
import ExperienceInteraction from './classes/experience-interaction.js';
|
||||
import { experienceRoutes } from './const.js';
|
||||
import koaExperienceInteraction, {
|
||||
type WithExperienceInteractionContext,
|
||||
} from './middleware/koa-experience-interaction.js';
|
||||
import { koaExperienceInteractionHooks } from './middleware/koa-experience-interaction-hooks.js';
|
||||
import koaExperienceInteraction from './middleware/koa-experience-interaction.js';
|
||||
import profileRoutes from './profile-routes.js';
|
||||
import { type ExperienceInteractionRouterContext } from './types.js';
|
||||
import backupCodeVerificationRoutes from './verification-routes/backup-code-verification.js';
|
||||
import enterpriseSsoVerificationRoutes from './verification-routes/enterprise-sso-verification.js';
|
||||
import newPasswordIdentityVerificationRoutes from './verification-routes/new-password-identity-verification.js';
|
||||
|
@ -39,13 +39,14 @@ type RouterContext<T> = T extends Router<unknown, infer Context> ? Context : nev
|
|||
export default function experienceApiRoutes<T extends AnonymousRouter>(
|
||||
...[anonymousRouter, tenant]: RouterInitArgs<T>
|
||||
) {
|
||||
const { queries } = tenant;
|
||||
const { provider, libraries } = tenant;
|
||||
|
||||
const experienceRouter =
|
||||
// @ts-expect-error for good koa types
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(anonymousRouter as Router<unknown, WithExperienceInteractionContext<RouterContext<T>>>).use(
|
||||
koaAuditLog(queries),
|
||||
(anonymousRouter as Router<unknown, ExperienceInteractionRouterContext<RouterContext<T>>>).use(
|
||||
koaInteractionDetails(provider),
|
||||
koaExperienceInteractionHooks(libraries),
|
||||
koaExperienceInteraction(tenant)
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
import { conditionalString, noop, trySafe } from '@silverhand/essentials';
|
||||
import { type MiddlewareType } from 'koa';
|
||||
import { type IRouterParamContext } from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
DataHookContextManager,
|
||||
InteractionHookContextManager,
|
||||
} from '#src/libraries/hook/context-manager.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||
|
||||
const interactionEventGuard = z.object({
|
||||
interactionEvent: z.nativeEnum(InteractionEvent),
|
||||
});
|
||||
|
||||
export type WithExperienceInteractionHooksContext<
|
||||
ContextT extends IRouterParamContext = IRouterParamContext,
|
||||
> = ContextT & {
|
||||
assignInteractionHookResult: InteractionHookContextManager['assignInteractionHookResult'];
|
||||
appendDataHookContext: DataHookContextManager['appendContext'];
|
||||
};
|
||||
|
||||
export function koaExperienceInteractionHooks<
|
||||
StateT,
|
||||
ContextT extends WithInteractionDetailsContext,
|
||||
ResponseT,
|
||||
>({
|
||||
hooks: { triggerInteractionHooks, triggerDataHooks },
|
||||
}: Libraries): MiddlewareType<StateT, WithExperienceInteractionHooksContext<ContextT>, ResponseT> {
|
||||
return async (ctx, next) => {
|
||||
const {
|
||||
interactionDetails,
|
||||
header: { 'user-agent': userAgent },
|
||||
ip,
|
||||
} = ctx;
|
||||
|
||||
// Get the interaction event from the interaction details
|
||||
const result = interactionEventGuard.safeParse(interactionDetails.result ?? {});
|
||||
|
||||
if (!result.success) {
|
||||
ctx.assignInteractionHookResult = noop;
|
||||
ctx.appendDataHookContext = noop;
|
||||
return next();
|
||||
}
|
||||
|
||||
const { interactionEvent } = result.data;
|
||||
const interactionApiMetadata = {
|
||||
interactionEvent,
|
||||
userAgent,
|
||||
applicationId: conditionalString(interactionDetails.params.client_id),
|
||||
sessionId: interactionDetails.jti,
|
||||
};
|
||||
const interactionHookContext = new InteractionHookContextManager({
|
||||
...interactionApiMetadata,
|
||||
userIp: ip,
|
||||
});
|
||||
|
||||
ctx.assignInteractionHookResult =
|
||||
interactionHookContext.assignInteractionHookResult.bind(interactionHookContext);
|
||||
|
||||
const dataHookContext = new DataHookContextManager({
|
||||
...interactionApiMetadata,
|
||||
ip,
|
||||
});
|
||||
|
||||
ctx.appendDataHookContext = dataHookContext.appendContext.bind(dataHookContext);
|
||||
|
||||
await next();
|
||||
|
||||
if (interactionHookContext.interactionHookResult) {
|
||||
// Hooks should not crash the app
|
||||
void trySafe(triggerInteractionHooks(getConsoleLogFromContext(ctx), interactionHookContext));
|
||||
}
|
||||
|
||||
if (dataHookContext.contextArray.length > 0) {
|
||||
// Hooks should not crash the app
|
||||
void trySafe(triggerDataHooks(getConsoleLogFromContext(ctx), dataHookContext));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
|
||||
import ExperienceInteraction from '../classes/experience-interaction.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithHooksAndLogsContext } from '../types.js';
|
||||
|
||||
export type WithExperienceInteractionContext<ContextT extends WithLogContext = WithLogContext> =
|
||||
ContextT & {
|
||||
|
@ -20,23 +21,23 @@ export type WithExperienceInteractionContext<ContextT extends WithLogContext = W
|
|||
*/
|
||||
export default function koaExperienceInteraction<
|
||||
StateT,
|
||||
ContextT extends WithLogContext,
|
||||
ContextT extends WithHooksAndLogsContext,
|
||||
ResponseT,
|
||||
>(
|
||||
tenant: TenantContext
|
||||
): MiddlewareType<StateT, WithExperienceInteractionContext<ContextT>, ResponseT> {
|
||||
return async (ctx, next) => {
|
||||
const { method, path } = ctx.request;
|
||||
const {
|
||||
interactionDetails,
|
||||
request: { method, path },
|
||||
} = ctx;
|
||||
|
||||
// Should not apply the koaExperienceInteraction middleware to the PUT /experience route.
|
||||
// New ExperienceInteraction instance are supposed to be created in the PUT /experience route.
|
||||
// Should not retrieve the interaction details for the PUT /experience request.
|
||||
// New ExperienceInteraction instance supposed to be created for this request.
|
||||
if (method === 'PUT' && path === `${experienceRoutes.prefix}`) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { provider } = tenant;
|
||||
const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
ctx.experienceInteraction = new ExperienceInteraction(ctx, tenant, interactionDetails);
|
||||
|
||||
return next();
|
||||
|
|
|
@ -16,7 +16,7 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
|
||||
import { identifierCodeVerificationTypeMap } from './classes/verifications/code-verification.js';
|
||||
import { experienceRoutes } from './const.js';
|
||||
import { type WithExperienceInteractionContext } from './middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from './types.js';
|
||||
|
||||
/**
|
||||
* @throws {RequestError} with status 400 if current interaction is ForgotPassword
|
||||
|
@ -27,7 +27,7 @@ function verifiedInteractionGuard<
|
|||
StateT,
|
||||
ContextT extends WithLogContext,
|
||||
ResponseT,
|
||||
>(): MiddlewareType<StateT, WithExperienceInteractionContext<ContextT>, ResponseT> {
|
||||
>(): MiddlewareType<StateT, ExperienceInteractionRouterContext<ContextT>, ResponseT> {
|
||||
return async (ctx, next) => {
|
||||
const { experienceInteraction } = ctx;
|
||||
|
||||
|
@ -47,8 +47,8 @@ function verifiedInteractionGuard<
|
|||
};
|
||||
}
|
||||
|
||||
export default function interactionProfileRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function interactionProfileRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
tenant: TenantContext
|
||||
) {
|
||||
router.post(
|
||||
|
|
|
@ -9,7 +9,12 @@ import {
|
|||
import type Provider from 'oidc-provider';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
|
||||
import { type VerificationRecordMap } from './classes/verifications/index.js';
|
||||
import { type WithExperienceInteractionHooksContext } from './middleware/koa-experience-interaction-hooks.js';
|
||||
import { type WithExperienceInteractionContext } from './middleware/koa-experience-interaction.js';
|
||||
|
||||
export type Interaction = Awaited<ReturnType<Provider['interactionDetails']>>;
|
||||
|
||||
|
@ -70,3 +75,13 @@ export type InteractionContext = {
|
|||
verificationId: string
|
||||
) => VerificationRecordMap[K];
|
||||
};
|
||||
|
||||
export type ExperienceInteractionRouterContext<ContextT extends WithLogContext = WithLogContext> =
|
||||
ContextT &
|
||||
WithInteractionDetailsContext &
|
||||
WithExperienceInteractionHooksContext &
|
||||
WithExperienceInteractionContext;
|
||||
|
||||
export type WithHooksAndLogsContext<ContextT extends WithLogContext = WithLogContext> = ContextT &
|
||||
WithInteractionDetailsContext &
|
||||
WithExperienceInteractionHooksContext;
|
||||
|
|
|
@ -2,17 +2,16 @@ import { backupCodeVerificationVerifyPayloadGuard } from '@logto/schemas';
|
|||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { BackupCodeVerification } from '../classes/verifications/backup-code-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function backupCodeVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function backupCodeVerificationRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
|
|
@ -7,19 +7,17 @@ import type Router from 'koa-router';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { EnterpriseSsoVerification } from '../classes/verifications/enterprise-sso-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function enterpriseSsoVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
export default function enterpriseSsoVerificationRoutes<
|
||||
T extends ExperienceInteractionRouterContext,
|
||||
>(router: Router<unknown, T>, tenantContext: TenantContext) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
||||
router.post(
|
||||
|
|
|
@ -2,18 +2,16 @@ import { newPasswordIdentityVerificationPayloadGuard } from '@logto/schemas';
|
|||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
import { NewPasswordIdentityVerification } from '../classes/verifications/new-password-identity-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function newPasswordIdentityVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
{ libraries, queries }: TenantContext
|
||||
) {
|
||||
export default function newPasswordIdentityVerificationRoutes<
|
||||
T extends ExperienceInteractionRouterContext,
|
||||
>(router: Router<unknown, T>, { libraries, queries }: TenantContext) {
|
||||
router.post(
|
||||
`${experienceRoutes.verification}/new-password-identity`,
|
||||
koaGuard({
|
||||
|
|
|
@ -2,16 +2,15 @@ import { passwordVerificationPayloadGuard } from '@logto/schemas';
|
|||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
import { PasswordVerification } from '../classes/verifications/password-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function passwordVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function passwordVerificationRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
{ libraries, queries }: TenantContext
|
||||
) {
|
||||
router.post(
|
||||
|
|
|
@ -7,17 +7,16 @@ import type Router from 'koa-router';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { SocialVerification } from '../classes/verifications/social-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function socialVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function socialVerificationRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
|
|
@ -3,17 +3,16 @@ import type Router from 'koa-router';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { TotpVerification } from '../classes/verifications/totp-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function totpVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function totpVerificationRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
|
|
@ -2,17 +2,16 @@ import { InteractionEvent, verificationCodeIdentifierGuard } from '@logto/schema
|
|||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
import { codeVerificationIdentifierRecordTypeMap } from '../classes/utils.js';
|
||||
import { createNewCodeVerificationRecord } from '../classes/verifications/code-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function verificationCodeRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function verificationCodeRoutes<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
{ libraries, queries }: TenantContext
|
||||
) {
|
||||
router.post(
|
||||
|
|
|
@ -8,17 +8,16 @@ import type Router from 'koa-router';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { WebAuthnVerification } from '../classes/verifications/web-authn-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
import { type ExperienceInteractionRouterContext } from '../types.js';
|
||||
|
||||
export default function webAuthnVerificationRoute<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
export default function webAuthnVerificationRoute<T extends ExperienceInteractionRouterContext>(
|
||||
router: Router<unknown, T>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
|
|
@ -3,6 +3,7 @@ import Koa from 'koa';
|
|||
import Router from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import koaAuditLog from '#src/middleware/koa-audit-log.js';
|
||||
import koaBodyEtag from '#src/middleware/koa-body-etag.js';
|
||||
import koaCors from '#src/middleware/koa-cors.js';
|
||||
import { koaManagementApiHooks } from '#src/middleware/koa-management-api-hooks.js';
|
||||
|
@ -53,6 +54,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
|
||||
const experienceRouter: AnonymousRouter = new Router();
|
||||
if (EnvSet.values.isDevFeaturesEnabled) {
|
||||
experienceRouter.use(koaAuditLog(tenant.queries));
|
||||
experienceApiRoutes(experienceRouter, tenant);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@ import { EnvSet } from '#src/env-set/index.js';
|
|||
import { assignInteractionResults } from '#src/libraries/session.js';
|
||||
import { encryptUserPassword } from '#src/libraries/user.utils.js';
|
||||
import type { LogEntry, WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||
import { buildAppInsightsTelemetry } from '#src/utils/request.js';
|
||||
import { getTenantId } from '#src/utils/tenant.js';
|
||||
|
||||
import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';
|
||||
import type {
|
||||
VerifiedInteractionResult,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { authenticator } from 'otplib';
|
|||
import qrcode from 'qrcode';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { WithInteractionDetailsContext } from '#src//middleware/koa-interaction-details.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
@ -20,7 +21,6 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
|
||||
import { parseUserProfile } from './actions/helpers.js';
|
||||
import { interactionPrefix, verificationPath } from './const.js';
|
||||
import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js';
|
||||
import { socialAuthorizationUrlPayloadGuard } from './types/guard.js';
|
||||
import {
|
||||
getInteractionStorage,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import {
|
||||
consentInfoResponseGuard,
|
||||
publicApplicationGuard,
|
||||
publicUserInfoGuard,
|
||||
applicationSignInExperienceGuard,
|
||||
type ConsentInfoResponse,
|
||||
consentInfoResponseGuard,
|
||||
Organizations,
|
||||
publicApplicationGuard,
|
||||
publicUserInfoGuard,
|
||||
} from '@logto/schemas';
|
||||
import { conditional, deduplicate } from '@silverhand/essentials';
|
||||
import type Router from 'koa-router';
|
||||
|
@ -15,11 +15,11 @@ import { z } from 'zod';
|
|||
|
||||
import { consent, getMissingScopes } from '#src/libraries/session.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { interactionPrefix } from '../const.js';
|
||||
import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
|
||||
import { filterAndParseMissingResourceScopes } from './utils.js';
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import { assignInteractionResults } from '#src/libraries/session.js';
|
||||
import koaAuditLog from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import koaInteractionDetails from '#src/middleware/koa-interaction-details.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AnonymousRouter, RouterInitArgs } from '../types.js';
|
||||
|
@ -16,34 +18,32 @@ import additionalRoutes from './additional.js';
|
|||
import consentRoutes from './consent/index.js';
|
||||
import { interactionPrefix } from './const.js';
|
||||
import mfaRoutes from './mfa.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 singleSignOnRoutes from './single-sign-on.js';
|
||||
import {
|
||||
getInteractionStorage,
|
||||
storeInteractionResult,
|
||||
mergeIdentifiers,
|
||||
isForgotPasswordInteractionResult,
|
||||
isSignInInteractionResult,
|
||||
mergeIdentifiers,
|
||||
storeInteractionResult,
|
||||
} from './utils/interaction.js';
|
||||
import {
|
||||
verifySignInModeSettings,
|
||||
verifyIdentifierSettings,
|
||||
verifyProfileSettings,
|
||||
verifySignInModeSettings,
|
||||
} from './utils/sign-in-experience-validation.js';
|
||||
import { verifySsoOnlyEmailIdentifier } from './utils/single-sign-on-guard.js';
|
||||
import { validatePassword } from './utils/validate-password.js';
|
||||
import {
|
||||
verifyIdentifierPayload,
|
||||
verifyIdentifier,
|
||||
verifyProfile,
|
||||
validateMandatoryUserProfile,
|
||||
validateMandatoryBindMfa,
|
||||
verifyBindMfa,
|
||||
verifyMfa,
|
||||
validateBindMfaBackupCode,
|
||||
validateMandatoryBindMfa,
|
||||
validateMandatoryUserProfile,
|
||||
verifyBindMfa,
|
||||
verifyIdentifier,
|
||||
verifyIdentifierPayload,
|
||||
verifyMfa,
|
||||
verifyProfile,
|
||||
} from './verifications/index.js';
|
||||
|
||||
export type RouterContext<T> = T extends Router<unknown, infer Context> ? Context : never;
|
||||
|
|
|
@ -12,11 +12,11 @@ import { z } from 'zod';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { interactionPrefix } from './const.js';
|
||||
import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js';
|
||||
import koaInteractionSie from './middleware/koa-interaction-sie.js';
|
||||
import { getInteractionStorage, storeInteractionResult } from './utils/interaction.js';
|
||||
import { verifyMfaSettings } from './utils/sign-in-experience-validation.js';
|
||||
|
|
|
@ -1,26 +1,17 @@
|
|||
import { type User } from '@logto/schemas';
|
||||
import { conditionalString, trySafe } from '@silverhand/essentials';
|
||||
import type { MiddlewareType } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
||||
import {
|
||||
type DataHookContext,
|
||||
DataHookContextManager,
|
||||
InteractionHookContextManager,
|
||||
} from '#src/libraries/hook/context-manager.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||
|
||||
import { getInteractionStorage } from '../utils/interaction.js';
|
||||
|
||||
import type { WithInteractionDetailsContext } from './koa-interaction-details.js';
|
||||
|
||||
type AppendDataHookContext = (
|
||||
payload: DataHookContext & {
|
||||
user?: User;
|
||||
}
|
||||
) => void;
|
||||
|
||||
export type WithInteractionHooksContext<
|
||||
ContextT extends IRouterParamContext = IRouterParamContext,
|
||||
> = ContextT & {
|
||||
|
|
|
@ -7,11 +7,11 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import { assignInteractionResults } from '#src/libraries/session.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { interactionPrefix, ssoPath } from './const.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 { getInteractionStorage, storeInteractionResult } from './utils/interaction.js';
|
||||
|
|
|
@ -4,6 +4,7 @@ import type Provider from 'oidc-provider';
|
|||
import { mockSsoConnector, wellConfiguredSsoConnector } from '#src/__mocks__/sso.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import { OidcSsoConnector } from '#src/sso/OidcSsoConnector/index.js';
|
||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||
import { type SingleSignOnConnectorData } from '#src/sso/types/connector.js';
|
||||
|
@ -12,7 +13,6 @@ import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
|||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
|
|
@ -4,16 +4,16 @@ import type { Nullable } from '@silverhand/essentials';
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
import type { WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
|
||||
import type {
|
||||
SocialIdentifier,
|
||||
VerifiedSignInInteractionResult,
|
||||
VerifiedRegisterInteractionResult,
|
||||
Identifier,
|
||||
SocialIdentifier,
|
||||
VerifiedRegisterInteractionResult,
|
||||
VerifiedSignInInteractionResult,
|
||||
} from '../types/index.js';
|
||||
import { isUserPasswordSet } from '../utils/index.js';
|
||||
import { mergeIdentifiers } from '../utils/interaction.js';
|
||||
|
|
|
@ -10,10 +10,10 @@ import type Provider from 'oidc-provider';
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
import { type WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
|
||||
import {
|
||||
type AccountVerifiedInteractionResult,
|
||||
|
|
|
@ -141,6 +141,22 @@ export const registerNewUserWithVerificationCode = async (
|
|||
return userId;
|
||||
};
|
||||
|
||||
export const identifyUserWithEmailVerificationCode = async (
|
||||
client: ExperienceClient,
|
||||
email: string
|
||||
) => {
|
||||
const { verificationId, code } = await successfullySendVerificationCode(client, {
|
||||
identifier: { type: SignInIdentifier.Email, value: email },
|
||||
interactionEvent: InteractionEvent.ForgotPassword,
|
||||
});
|
||||
await successfullyVerifyVerificationCode(client, {
|
||||
identifier: { type: SignInIdentifier.Email, value: email },
|
||||
verificationId,
|
||||
code,
|
||||
});
|
||||
await client.identifyUser({ verificationId });
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param socialUserInfo The social user info that will be returned by the social connector.
|
||||
|
@ -265,3 +281,18 @@ export const registerNewUserUsernamePassword = async (username: string, password
|
|||
|
||||
return userId;
|
||||
};
|
||||
|
||||
export const fulfillUserEmail = async (client: ExperienceClient, email: string) => {
|
||||
const { verificationId, code } = await successfullySendVerificationCode(client, {
|
||||
identifier: { type: SignInIdentifier.Email, value: email },
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await successfullyVerifyVerificationCode(client, {
|
||||
identifier: { type: SignInIdentifier.Email, value: email },
|
||||
verificationId,
|
||||
code,
|
||||
});
|
||||
|
||||
await client.updateProfile({ type: SignInIdentifier.Email, verificationId });
|
||||
};
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
import {
|
||||
hookEvents,
|
||||
InteractionEvent,
|
||||
InteractionHookEvent,
|
||||
SignInIdentifier,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { SsoConnectorApi } from '#src/api/sso-connector.js';
|
||||
import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js';
|
||||
import { resetPasswordlessConnectors } from '#src/helpers/connector.js';
|
||||
import {
|
||||
fulfillUserEmail,
|
||||
identifyUserWithEmailVerificationCode,
|
||||
identifyUserWithUsernamePassword,
|
||||
registerNewUserUsernamePassword,
|
||||
registerNewUserWithVerificationCode,
|
||||
signInWithEnterpriseSso,
|
||||
signInWithPassword,
|
||||
} from '#src/helpers/experience/index.js';
|
||||
import { WebHookApiTest } from '#src/helpers/hook.js';
|
||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||
import {
|
||||
enableAllPasswordSignInMethods,
|
||||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
|
||||
import { devFeatureTest, generateEmail, generatePassword, randomString } from '#src/utils.js';
|
||||
|
||||
import WebhookMockServer from './WebhookMockServer.js';
|
||||
import { assertHookLogResult } from './utils.js';
|
||||
|
||||
const webbHookMockServer = new WebhookMockServer(9999);
|
||||
const userNamePrefix = 'experienceApiHookTriggerTestUser';
|
||||
const username = `${userNamePrefix}_0`;
|
||||
const password = generatePassword();
|
||||
// For email fulfilling and reset password use
|
||||
const email = generateEmail();
|
||||
|
||||
const userApi = new UserApiTest();
|
||||
const webHookApi = new WebHookApiTest();
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
const ssoConnectorApi = new SsoConnectorApi();
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
resetPasswordlessConnectors(),
|
||||
enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
}),
|
||||
webbHookMockServer.listen(),
|
||||
userApi.create({ username, password }),
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([userApi.cleanUp(), webbHookMockServer.close()]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([organizationApi.cleanUp(), ssoConnectorApi.cleanUp()]);
|
||||
});
|
||||
|
||||
devFeatureTest.describe('trigger invalid hook', () => {
|
||||
beforeAll(async () => {
|
||||
await webHookApi.create({
|
||||
name: 'invalidHookEventListener',
|
||||
events: [InteractionHookEvent.PostSignIn],
|
||||
config: { url: 'not_work_url' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should log invalid hook url error', async () => {
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
const hook = webHookApi.hooks.get('invalidHookEventListener')!;
|
||||
|
||||
await assertHookLogResult(hook, InteractionHookEvent.PostSignIn, {
|
||||
errorMessage: 'Failed to parse URL from not_work_url',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await webHookApi.cleanUp();
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('experience api hook trigger', () => {
|
||||
// Use new hooks for each test to ensure test isolation
|
||||
beforeEach(async () => {
|
||||
await Promise.all([
|
||||
webHookApi.create({
|
||||
name: 'interactionHookEventListener',
|
||||
events: Object.values(InteractionHookEvent),
|
||||
config: { url: webbHookMockServer.endpoint },
|
||||
}),
|
||||
webHookApi.create({
|
||||
name: 'dataHookEventListener',
|
||||
events: hookEvents.filter((event) => !(event in InteractionHookEvent)),
|
||||
config: { url: webbHookMockServer.endpoint },
|
||||
}),
|
||||
webHookApi.create({
|
||||
name: 'registerOnlyInteractionHookEventListener',
|
||||
events: [InteractionHookEvent.PostRegister],
|
||||
config: { url: webbHookMockServer.endpoint },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await webHookApi.cleanUp();
|
||||
});
|
||||
|
||||
it('new user registration interaction API', async () => {
|
||||
const interactionHook = webHookApi.hooks.get('interactionHookEventListener')!;
|
||||
const registerHook = webHookApi.hooks.get('registerOnlyInteractionHookEventListener')!;
|
||||
const dataHook = webHookApi.hooks.get('dataHookEventListener')!;
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const userId = await registerNewUserUsernamePassword(username, password);
|
||||
|
||||
const interactionHookEventPayload: Record<string, unknown> = {
|
||||
event: InteractionHookEvent.PostRegister,
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
sessionId: expect.any(String),
|
||||
user: expect.objectContaining({ id: userId, username }),
|
||||
};
|
||||
|
||||
await assertHookLogResult(interactionHook, InteractionHookEvent.PostRegister, {
|
||||
hookPayload: interactionHookEventPayload,
|
||||
});
|
||||
|
||||
// Verify multiple hooks can be triggered with the same event
|
||||
await assertHookLogResult(registerHook, InteractionHookEvent.PostRegister, {
|
||||
hookPayload: interactionHookEventPayload,
|
||||
});
|
||||
|
||||
// Verify data hook is triggered
|
||||
await assertHookLogResult(dataHook, 'User.Created', {
|
||||
hookPayload: {
|
||||
event: 'User.Created',
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
sessionId: expect.any(String),
|
||||
data: expect.objectContaining({ id: userId, username }),
|
||||
},
|
||||
});
|
||||
|
||||
// Assert user updated event is not triggered
|
||||
await assertHookLogResult(dataHook, 'User.Data.Updated', {
|
||||
toBeUndefined: true,
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('user sign in interaction API without profile update', async () => {
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
const interactionHook = webHookApi.hooks.get('interactionHookEventListener')!;
|
||||
const dataHook = webHookApi.hooks.get('dataHookEventListener')!;
|
||||
const user = userApi.users.find(({ username: name }) => name === username)!;
|
||||
|
||||
const interactionHookEventPayload: Record<string, unknown> = {
|
||||
event: InteractionHookEvent.PostSignIn,
|
||||
interactionEvent: InteractionEvent.SignIn,
|
||||
sessionId: expect.any(String),
|
||||
user: expect.objectContaining({ id: user.id, username }),
|
||||
};
|
||||
|
||||
await assertHookLogResult(interactionHook, InteractionHookEvent.PostSignIn, {
|
||||
hookPayload: interactionHookEventPayload,
|
||||
});
|
||||
|
||||
// Verify user create data hook is not triggered
|
||||
await assertHookLogResult(dataHook, 'User.Created', {
|
||||
toBeUndefined: true,
|
||||
});
|
||||
|
||||
await assertHookLogResult(dataHook, 'User.Data.Updated', {
|
||||
toBeUndefined: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('user sign in interaction API with profile update', async () => {
|
||||
await enableAllVerificationCodeSignInMethods({
|
||||
identifiers: [SignInIdentifier.Email],
|
||||
password: false,
|
||||
verify: true,
|
||||
});
|
||||
|
||||
const interactionHook = webHookApi.hooks.get('interactionHookEventListener')!;
|
||||
const dataHook = webHookApi.hooks.get('dataHookEventListener')!;
|
||||
const user = userApi.users.find(({ username: name }) => name === username)!;
|
||||
|
||||
const client = await initExperienceClient();
|
||||
|
||||
await identifyUserWithUsernamePassword(client, username, password);
|
||||
await fulfillUserEmail(client, email);
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
const interactionHookEventPayload: Record<string, unknown> = {
|
||||
event: InteractionHookEvent.PostSignIn,
|
||||
interactionEvent: InteractionEvent.SignIn,
|
||||
sessionId: expect.any(String),
|
||||
user: expect.objectContaining({ id: user.id, username }),
|
||||
};
|
||||
|
||||
await assertHookLogResult(interactionHook, InteractionHookEvent.PostSignIn, {
|
||||
hookPayload: interactionHookEventPayload,
|
||||
});
|
||||
|
||||
// Verify user create data hook is not triggered
|
||||
await assertHookLogResult(dataHook, 'User.Created', {
|
||||
toBeUndefined: true,
|
||||
});
|
||||
|
||||
await assertHookLogResult(dataHook, 'User.Data.Updated', {
|
||||
hookPayload: {
|
||||
event: 'User.Data.Updated',
|
||||
interactionEvent: InteractionEvent.SignIn,
|
||||
sessionId: expect.any(String),
|
||||
data: expect.objectContaining({ id: user.id, username, primaryEmail: email }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('password reset interaction API', async () => {
|
||||
const newPassword = generatePassword();
|
||||
const interactionHook = webHookApi.hooks.get('interactionHookEventListener')!;
|
||||
const dataHook = webHookApi.hooks.get('dataHookEventListener')!;
|
||||
const user = userApi.users.find(({ username: name }) => name === username)!;
|
||||
|
||||
const client = await initExperienceClient(InteractionEvent.ForgotPassword);
|
||||
await identifyUserWithEmailVerificationCode(client, email);
|
||||
await client.resetPassword({ password: newPassword });
|
||||
await client.submitInteraction();
|
||||
|
||||
const interactionHookEventPayload: Record<string, unknown> = {
|
||||
event: InteractionHookEvent.PostResetPassword,
|
||||
interactionEvent: InteractionEvent.ForgotPassword,
|
||||
sessionId: expect.any(String),
|
||||
user: expect.objectContaining({ id: user.id, username, primaryEmail: email }),
|
||||
};
|
||||
|
||||
await assertHookLogResult(interactionHook, InteractionHookEvent.PostResetPassword, {
|
||||
hookPayload: interactionHookEventPayload,
|
||||
});
|
||||
|
||||
await assertHookLogResult(dataHook, 'User.Data.Updated', {
|
||||
hookPayload: {
|
||||
event: 'User.Data.Updated',
|
||||
interactionEvent: InteractionEvent.ForgotPassword,
|
||||
sessionId: expect.any(String),
|
||||
data: expect.objectContaining({ id: user.id, username, primaryEmail: email }),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('organization jit provisioning hook trigger', () => {
|
||||
const hookName = 'organizationJitProvisioningHookEventListener';
|
||||
|
||||
beforeAll(async () => {
|
||||
await webHookApi.create({
|
||||
name: hookName,
|
||||
events: ['Organization.Membership.Updated'],
|
||||
config: { url: webbHookMockServer.endpoint },
|
||||
});
|
||||
});
|
||||
|
||||
const assertOrganizationMembershipUpdated = async (organizationId: string) =>
|
||||
assertHookLogResult(webHookApi.hooks.get(hookName)!, 'Organization.Membership.Updated', {
|
||||
hookPayload: {
|
||||
event: 'Organization.Membership.Updated',
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
it('should trigger `Organization.Membership.Updated` event when user is provisioned by experience', async () => {
|
||||
const organization = await organizationApi.create({ name: 'foo' });
|
||||
const domain = 'example.com';
|
||||
await organizationApi.jit.addEmailDomain(organization.id, domain);
|
||||
|
||||
const userId = await registerNewUserWithVerificationCode({
|
||||
type: SignInIdentifier.Email,
|
||||
value: generateEmail(domain),
|
||||
});
|
||||
await assertOrganizationMembershipUpdated(organization.id);
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('should trigger `Organization.Membership.Updated` event when user is provisioned by SSO', async () => {
|
||||
const organization = await organizationApi.create({ name: 'bar' });
|
||||
const domain = 'sso_example.com';
|
||||
const connector = await ssoConnectorApi.createMockOidcConnector([domain]);
|
||||
await organizationApi.jit.ssoConnectors.add(organization.id, [connector.id]);
|
||||
const email = generateEmail(domain);
|
||||
const sub = randomString();
|
||||
|
||||
await updateSignInExperience({
|
||||
singleSignOnEnabled: true,
|
||||
});
|
||||
|
||||
const userId = await signInWithEnterpriseSso(
|
||||
ssoConnectorApi.firstConnectorId!,
|
||||
{
|
||||
sub,
|
||||
email,
|
||||
email_verified: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
await assertOrganizationMembershipUpdated(organization.id);
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue