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

feat(core,schemas): add auditLogs to experience API (#6361)

* 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,schemas): add auditLogs to experience API

add auditLogs to experience API
This commit is contained in:
simeng-li 2024-07-31 18:38:34 +08:00 committed by GitHub
parent 84f7e13a2b
commit cf31e3a5af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 341 additions and 35 deletions

View file

@ -5,6 +5,7 @@ import { conditional } from '@silverhand/essentials';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import { type LogEntry } from '#src/middleware/koa-audit-log.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';
@ -180,9 +181,13 @@ export default class ExperienceInteraction {
* @see {@link identifyExistingUser} for more exceptions that can be thrown in the SignIn and ForgotPassword events.
* @see {@link createNewUser} for more exceptions that can be thrown in the Register event.
**/
public async identifyUser(verificationId: string, linkSocialIdentity?: boolean) {
public async identifyUser(verificationId: string, linkSocialIdentity?: boolean, log?: LogEntry) {
const verificationRecord = this.getVerificationRecordById(verificationId);
log?.append({
verification: verificationRecord?.toJson(),
});
assertThat(
this.interactionEvent,
new RequestError({ code: 'session.interaction_not_found', status: 404 })
@ -442,6 +447,8 @@ export default class ExperienceInteraction {
// Note: The profile data is not saved to the user profile until the user submits the interaction.
// Also no need to validate the synced profile data availability as it is already validated during the identification process.
if (syncedProfile) {
const log = this.ctx.createLog(`Interaction.${this.interactionEvent}.Profile.Update`);
log.append({ syncedProfile });
this.profile.unsafeSet(syncedProfile);
}
}

View file

@ -18,6 +18,7 @@ import { deduplicate } from '@silverhand/essentials';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import { type LogEntry } from '#src/middleware/koa-audit-log.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';
@ -170,11 +171,16 @@ export class Mfa {
*
* - Any existing TOTP factor will be replaced with the new one.
*/
async addTotpByVerificationId(verificationId: string) {
async addTotpByVerificationId(verificationId: string, log?: LogEntry) {
const verificationRecord = this.interactionContext.getVerificationRecordByTypeAndId(
VerificationType.TOTP,
verificationId
);
log?.append({
verification: verificationRecord.toJson(),
});
const bindTotp = verificationRecord.toBindMfa();
await this.checkMfaFactorsEnabledInSignInExperience([MfaFactor.TOTP]);
@ -198,11 +204,16 @@ export class Mfa {
* @throws {RequestError} with status 404 if the verification record is not found
* @throws {RequestError} with status 400 if WebAuthn is not enabled in the sign-in experience
*/
async addWebAuthnByVerificationId(verificationId: string) {
async addWebAuthnByVerificationId(verificationId: string, log?: LogEntry) {
const verificationRecord = this.interactionContext.getVerificationRecordByTypeAndId(
VerificationType.WebAuthn,
verificationId
);
log?.append({
verification: verificationRecord.toJson(),
});
const bindWebAuthn = verificationRecord.toBindMfa();
await this.checkMfaFactorsEnabledInSignInExperience([MfaFactor.WebAuthn]);
@ -218,12 +229,16 @@ export class Mfa {
* @throws {RequestError} with status 400 if Backup Code is not enabled in the sign-in experience
* @throws {RequestError} with status 422 if the backup code is the only MFA factor
*/
async addBackupCodeByVerificationId(verificationId: string) {
async addBackupCodeByVerificationId(verificationId: string, log?: LogEntry) {
const verificationRecord = this.interactionContext.getVerificationRecordByTypeAndId(
VerificationType.BackupCode,
verificationId
);
log?.append({
verification: verificationRecord.toJson(),
});
await this.checkMfaFactorsEnabledInSignInExperience([MfaFactor.BackupCode]);
const { mfaVerifications } = await this.interactionContext.getIdentifiedUser();

View file

@ -1,6 +1,7 @@
import { type VerificationType } from '@logto/schemas';
import RequestError from '#src/errors/RequestError/index.js';
import { type LogEntry } from '#src/middleware/koa-audit-log.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
@ -38,12 +39,18 @@ export class Profile {
*/
async setProfileByVerificationRecord(
type: VerificationType.EmailVerificationCode | VerificationType.PhoneVerificationCode,
verificationId: string
verificationId: string,
log?: LogEntry
) {
const verificationRecord = this.interactionContext.getVerificationRecordByTypeAndId(
type,
verificationId
);
log?.append({
verification: verificationRecord.toJson(),
});
const profile = verificationRecord.toUserProfile();
await this.setProfileWithValidation(profile);
}

View file

@ -62,7 +62,7 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
const { interactionEvent } = ctx.guard.body;
const { createLog } = ctx;
createLog(`Interaction.${interactionEvent}.Update`);
createLog(`Interaction.${interactionEvent}.Create`);
const experienceInteraction = new ExperienceInteraction(ctx, tenant, interactionEvent);
@ -70,6 +70,8 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
// This will overwrite any existing interaction data in the storage.
await experienceInteraction.save();
ctx.experienceInteraction = experienceInteraction;
ctx.status = 204;
return next();
@ -89,13 +91,12 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
const { createLog, experienceInteraction } = ctx;
const eventLog = createLog(`Interaction.${experienceInteraction.interactionEvent}.Update`);
await experienceInteraction.setInteractionEvent(interactionEvent);
eventLog.append({
interactionEvent,
});
await experienceInteraction.setInteractionEvent(interactionEvent);
await experienceInteraction.save();
ctx.status = 204;
@ -112,9 +113,20 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
}),
async (ctx, next) => {
const { verificationId, linkSocialIdentity } = ctx.guard.body;
const { experienceInteraction } = ctx;
const { experienceInteraction, createLog } = ctx;
await experienceInteraction.identifyUser(verificationId, linkSocialIdentity);
const log = createLog(
`Interaction.${experienceInteraction.interactionEvent}.Identifier.Submit`
);
log.append({
payload: {
verificationId,
linkSocialIdentity,
},
});
await experienceInteraction.identifyUser(verificationId, linkSocialIdentity, log);
await experienceInteraction.save();
@ -136,7 +148,16 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
.optional(),
}),
async (ctx, next) => {
const { createLog, experienceInteraction } = ctx;
const log = createLog(`Interaction.${experienceInteraction.interactionEvent}.Submit`);
await ctx.experienceInteraction.submit();
log.append({
interaction: ctx.experienceInteraction.toJson(),
});
ctx.status = 200;
return next();
}

View file

@ -1,16 +1,17 @@
import type { MiddlewareType } from 'koa';
import { type IRouterParamContext } from 'koa-router';
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
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 & {
experienceInteraction: ExperienceInteraction;
};
export type WithExperienceInteractionContext<
ContextT extends IRouterParamContext = IRouterParamContext,
> = ContextT & {
experienceInteraction: ExperienceInteraction;
};
/**
* @overview This middleware initializes the `ExperienceInteraction` for the current request.
@ -40,6 +41,17 @@ export default function koaExperienceInteraction<
ctx.experienceInteraction = new ExperienceInteraction(ctx, tenant, interactionDetails);
return next();
try {
await next();
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- make sure the interaction is initialized
if (ctx.experienceInteraction) {
ctx.prependAllLogEntries({
interaction: ctx.experienceInteraction.toJson(),
});
}
throw error;
}
};
}

View file

@ -0,0 +1,36 @@
import { type VerificationType } from '@logto/schemas';
import { type Action } from '@logto/schemas/lib/types/log/interaction.js';
import { type MiddlewareType } from 'koa';
import { type IRouterParamContext } from 'koa-router';
import { type LogContext, type LogEntry } from '#src/middleware/koa-audit-log.js';
import { type WithExperienceInteractionContext } from './koa-experience-interaction.js';
type WithExperienceVerificationAuditLogContext<ContextT extends IRouterParamContext> = ContextT & {
verificationAuditLog: LogEntry;
};
export default function koaExperienceVerificationsAuditLog<
StateT,
ContextT extends WithExperienceInteractionContext & LogContext,
ResponseT,
>({
type,
action,
}: {
type: VerificationType;
action: Action;
}): MiddlewareType<StateT, WithExperienceVerificationAuditLogContext<ContextT>, ResponseT> {
return async (ctx, next) => {
const { experienceInteraction, createLog } = ctx;
const log = createLog(
`Interaction.${experienceInteraction.interactionEvent}.Verification.${type}.${action}`
);
ctx.verificationAuditLog = log;
return next();
};
}

View file

@ -59,9 +59,15 @@ export default function interactionProfileRoutes<T extends ExperienceInteraction
}),
verifiedInteractionGuard(),
async (ctx, next) => {
const { experienceInteraction, guard } = ctx;
const { experienceInteraction, guard, createLog } = ctx;
const profilePayload = guard.body;
const log = createLog(`Interaction.${experienceInteraction.interactionEvent}.Profile.Update`);
log.append({
payload: profilePayload,
});
switch (profilePayload.type) {
case SignInIdentifier.Email:
case SignInIdentifier.Phone: {
@ -100,7 +106,7 @@ export default function interactionProfileRoutes<T extends ExperienceInteraction
status: [204, 400, 404, 422],
}),
async (ctx, next) => {
const { experienceInteraction, guard } = ctx;
const { experienceInteraction, guard, createLog } = ctx;
const { password } = guard.body;
assertThat(
@ -111,6 +117,8 @@ export default function interactionProfileRoutes<T extends ExperienceInteraction
})
);
createLog(`Interaction.ForgotPassword.Profile.Update`);
// Guard interaction is identified
assertThat(
experienceInteraction.identifiedUserId,
@ -159,17 +167,25 @@ export default function interactionProfileRoutes<T extends ExperienceInteraction
const { experienceInteraction, guard } = ctx;
const { type, verificationId } = guard.body;
const log = ctx.createLog(
`Interaction.${experienceInteraction.interactionEvent}.BindMfa.${type}.Submit`
);
log.append({
verificationId,
});
switch (type) {
case MfaFactor.TOTP: {
await experienceInteraction.mfa.addTotpByVerificationId(verificationId);
await experienceInteraction.mfa.addTotpByVerificationId(verificationId, log);
break;
}
case MfaFactor.WebAuthn: {
await experienceInteraction.mfa.addWebAuthnByVerificationId(verificationId);
await experienceInteraction.mfa.addWebAuthnByVerificationId(verificationId, log);
break;
}
case MfaFactor.BackupCode: {
await experienceInteraction.mfa.addBackupCodeByVerificationId(verificationId);
await experienceInteraction.mfa.addBackupCodeByVerificationId(verificationId, log);
break;
}
}

View file

@ -1,4 +1,5 @@
import { backupCodeVerificationVerifyPayloadGuard } from '@logto/schemas';
import { backupCodeVerificationVerifyPayloadGuard, VerificationType } from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -8,6 +9,7 @@ import assertThat from '#src/utils/assert-that.js';
import { BackupCodeVerification } from '../classes/verifications/backup-code-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function backupCodeVerificationRoutes<T extends ExperienceInteractionRouterContext>(
@ -25,6 +27,10 @@ export default function backupCodeVerificationRoutes<T extends ExperienceInterac
codes: z.array(z.string()),
}),
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.BackupCode,
action: Action.Create,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
@ -60,10 +66,20 @@ export default function backupCodeVerificationRoutes<T extends ExperienceInterac
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.BackupCode,
action: Action.Submit,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
const { experienceInteraction, verificationAuditLog } = ctx;
const { code } = ctx.guard.body;
verificationAuditLog.append({
payload: {
code,
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
const backupCodeVerificationRecord = BackupCodeVerification.create(

View file

@ -3,6 +3,7 @@ import {
socialAuthorizationUrlPayloadGuard,
socialVerificationCallbackPayloadGuard,
} from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -13,6 +14,7 @@ import assertThat from '#src/utils/assert-that.js';
import { EnterpriseSsoVerification } from '../classes/verifications/enterprise-sso-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function enterpriseSsoVerificationRoutes<
@ -33,9 +35,20 @@ export default function enterpriseSsoVerificationRoutes<
}),
status: [200, 400, 404, 500],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.EnterpriseSso,
action: Action.Create,
}),
async (ctx, next) => {
const { connectorId } = ctx.guard.params;
ctx.verificationAuditLog.append({
payload: {
connectorId,
...ctx.guard.body,
},
});
const enterpriseSsoVerification = EnterpriseSsoVerification.create(
libraries,
queries,
@ -73,10 +86,22 @@ export default function enterpriseSsoVerificationRoutes<
}),
status: [200, 400, 404, 500],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.EnterpriseSso,
action: Action.Submit,
}),
async (ctx, next) => {
const { connectorId } = ctx.params;
const { connectorData, verificationId } = ctx.guard.body;
ctx.verificationAuditLog.append({
payload: {
connectorId,
verificationId,
connectorData,
},
});
const enterpriseSsoVerificationRecord =
ctx.experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.EnterpriseSso,

View file

@ -1,4 +1,5 @@
import { newPasswordIdentityVerificationPayloadGuard } from '@logto/schemas';
import { newPasswordIdentityVerificationPayloadGuard, VerificationType } from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -7,6 +8,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
import { NewPasswordIdentityVerification } from '../classes/verifications/new-password-identity-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function newPasswordIdentityVerificationRoutes<
@ -21,9 +23,20 @@ export default function newPasswordIdentityVerificationRoutes<
verificationId: z.string(),
}),
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.NewPasswordIdentity,
action: Action.Submit,
}),
async (ctx, next) => {
const { identifier, password } = ctx.guard.body;
const { experienceInteraction } = ctx;
const { experienceInteraction, verificationAuditLog } = ctx;
verificationAuditLog.append({
payload: {
identifier,
password,
},
});
const newPasswordIdentityVerification = NewPasswordIdentityVerification.create(
libraries,

View file

@ -1,4 +1,5 @@
import { passwordVerificationPayloadGuard } from '@logto/schemas';
import { passwordVerificationPayloadGuard, VerificationType } from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -7,6 +8,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
import { PasswordVerification } from '../classes/verifications/password-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function passwordVerificationRoutes<T extends ExperienceInteractionRouterContext>(
@ -22,9 +24,20 @@ export default function passwordVerificationRoutes<T extends ExperienceInteracti
verificationId: z.string(),
}),
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.Password,
action: Action.Submit,
}),
async (ctx, next) => {
const { identifier, password } = ctx.guard.body;
ctx.verificationAuditLog.append({
payload: {
identifier,
password,
},
});
const passwordVerification = PasswordVerification.create(libraries, queries, identifier);
await passwordVerification.verify(password);
ctx.experienceInteraction.setVerificationRecord(passwordVerification);

View file

@ -3,6 +3,7 @@ import {
socialAuthorizationUrlPayloadGuard,
socialVerificationCallbackPayloadGuard,
} from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -13,6 +14,7 @@ import assertThat from '#src/utils/assert-that.js';
import { SocialVerification } from '../classes/verifications/social-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function socialVerificationRoutes<T extends ExperienceInteractionRouterContext>(
@ -34,8 +36,20 @@ export default function socialVerificationRoutes<T extends ExperienceInteraction
}),
status: [200, 400, 404, 500],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.Social,
action: Action.Create,
}),
async (ctx, next) => {
const { connectorId } = ctx.guard.params;
const { verificationAuditLog } = ctx;
verificationAuditLog.append({
payload: {
connectorId,
...ctx.guard.body,
},
});
const socialVerification = SocialVerification.create(libraries, queries, connectorId);
@ -70,9 +84,22 @@ export default function socialVerificationRoutes<T extends ExperienceInteraction
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.Social,
action: Action.Submit,
}),
async (ctx, next) => {
const { connectorId } = ctx.params;
const { connectorData, verificationId } = ctx.guard.body;
const { verificationAuditLog } = ctx;
verificationAuditLog.append({
payload: {
connectorId,
verificationId,
connectorData,
},
});
const socialVerificationRecord = ctx.experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.Social,
@ -85,7 +112,6 @@ export default function socialVerificationRoutes<T extends ExperienceInteraction
);
await socialVerificationRecord.verify(ctx, tenantContext, connectorData);
await ctx.experienceInteraction.save();
ctx.body = {

View file

@ -1,4 +1,5 @@
import { VerificationType, totpVerificationVerifyPayloadGuard } from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -9,6 +10,7 @@ import assertThat from '#src/utils/assert-that.js';
import { TotpVerification } from '../classes/verifications/totp-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function totpVerificationRoutes<T extends ExperienceInteractionRouterContext>(
@ -27,6 +29,10 @@ export default function totpVerificationRoutes<T extends ExperienceInteractionRo
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.TOTP,
action: Action.Create,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
@ -63,10 +69,21 @@ export default function totpVerificationRoutes<T extends ExperienceInteractionRo
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.TOTP,
action: Action.Submit,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
const { experienceInteraction, verificationAuditLog } = ctx;
const { verificationId, code } = ctx.guard.body;
verificationAuditLog.append({
payload: {
verificationId,
code,
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
// Verify new generated secret

View file

@ -1,15 +1,33 @@
import { InteractionEvent, verificationCodeIdentifierGuard } from '@logto/schemas';
import {
InteractionEvent,
type VerificationCodeIdentifier,
verificationCodeIdentifierGuard,
} from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
import { type LogContext } from '#src/middleware/koa-audit-log.js';
import koaGuard from '#src/middleware/koa-guard.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import type ExperienceInteraction from '../classes/experience-interaction.js';
import { codeVerificationIdentifierRecordTypeMap } from '../classes/utils.js';
import { createNewCodeVerificationRecord } from '../classes/verifications/code-verification.js';
import { experienceRoutes } from '../const.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
const createVerificationCodeAuditLog = (
{ createLog }: LogContext,
{ interactionEvent }: ExperienceInteraction,
identifier: VerificationCodeIdentifier,
action: Action
) => {
const verificationType = codeVerificationIdentifierRecordTypeMap[identifier.type];
return createLog(`Interaction.${interactionEvent}.Verification.${verificationType}.${action}`);
};
export default function verificationCodeRoutes<T extends ExperienceInteractionRouterContext>(
router: Router<unknown, T>,
{ libraries, queries }: TenantContext
@ -30,6 +48,20 @@ export default function verificationCodeRoutes<T extends ExperienceInteractionRo
async (ctx, next) => {
const { identifier, interactionEvent } = ctx.guard.body;
const log = createVerificationCodeAuditLog(
ctx,
ctx.experienceInteraction,
identifier,
Action.Create
);
log.append({
payload: {
identifier,
interactionEvent,
},
});
const codeVerification = createNewCodeVerificationRecord(
libraries,
queries,
@ -68,6 +100,21 @@ export default function verificationCodeRoutes<T extends ExperienceInteractionRo
async (ctx, next) => {
const { verificationId, code, identifier } = ctx.guard.body;
const log = createVerificationCodeAuditLog(
ctx,
ctx.experienceInteraction,
identifier,
Action.Submit
);
log.append({
payload: {
identifier,
verificationId,
code,
},
});
const codeVerificationRecord = ctx.experienceInteraction.getVerificationRecordByTypeAndId(
codeVerificationIdentifierRecordTypeMap[identifier.type],
verificationId

View file

@ -4,6 +4,7 @@ import {
webAuthnRegistrationOptionsGuard,
webAuthnVerificationPayloadGuard,
} from '@logto/schemas';
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
@ -14,6 +15,7 @@ import assertThat from '#src/utils/assert-that.js';
import { WebAuthnVerification } from '../classes/verifications/web-authn-verification.js';
import { experienceRoutes } from '../const.js';
import koaExperienceVerificationsAuditLog from '../middleware/koa-experience-verifications-audit-log.js';
import { type ExperienceInteractionRouterContext } from '../types.js';
export default function webAuthnVerificationRoute<T extends ExperienceInteractionRouterContext>(
@ -31,6 +33,10 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.WebAuthn,
action: Action.Create,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
@ -73,10 +79,21 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.WebAuthn,
action: Action.Submit,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
const { experienceInteraction, verificationAuditLog } = ctx;
const { verificationId, payload } = ctx.guard.body;
verificationAuditLog.append({
payload: {
verificationId,
payload,
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
const webAuthnVerification = experienceInteraction.getVerificationRecordByTypeAndId(
@ -115,6 +132,10 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.WebAuthn,
action: Action.Create,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
@ -156,10 +177,21 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
}),
status: [200, 400, 404],
}),
koaExperienceVerificationsAuditLog({
type: VerificationType.WebAuthn,
action: Action.Submit,
}),
async (ctx, next) => {
const { experienceInteraction } = ctx;
const { experienceInteraction, verificationAuditLog } = ctx;
const { verificationId, payload } = ctx.guard.body;
verificationAuditLog.append({
payload: {
verificationId,
payload,
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
const webAuthnVerification = experienceInteraction.getVerificationRecordByTypeAndId(

View file

@ -1,5 +1,5 @@
import { type MfaFactor } from '../../foundations/index.js';
import type { InteractionEvent } from '../interactions.js';
import type { InteractionEvent, VerificationType } from '../interactions.js';
export type Prefix = 'Interaction';
@ -12,6 +12,7 @@ export enum Field {
Profile = 'Profile',
BindMfa = 'BindMfa',
Mfa = 'Mfa',
Verification = 'Verification',
}
/** Method to verify the identifier */
@ -74,7 +75,7 @@ export enum Action {
*/
export type LogKey =
| `${Prefix}.${Action.Create | Action.End}`
| `${Prefix}.${InteractionEvent}.${Action.Update | Action.Submit}`
| `${Prefix}.${InteractionEvent}.${Action.Create | Action.Update | Action.Submit}`
| `${Prefix}.${InteractionEvent}.${Field.Profile}.${
| Action.Update // PATCH profile
| Action.Create // PUT profile
@ -93,4 +94,6 @@ export type LogKey =
| `${Prefix}.${InteractionEvent}.${Field.BindMfa}.${MfaFactor}.${Action.Submit | Action.Create}`
| `${Prefix}.${InteractionEvent.SignIn}.${Field.Mfa}.${MfaFactor}.${
| Action.Submit
| Action.Create}`;
| Action.Create}`
| `${Prefix}.${InteractionEvent}.${Field.Verification}.${VerificationType}.${Action}`
| `${Prefix}.${InteractionEvent}.${Field.Identifier}.${Action.Submit}`;