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

feat(core): add SAML app audit logs (#6931)

This commit is contained in:
Darcy Ye 2025-01-10 16:12:23 +08:00 committed by GitHub
parent d16666a471
commit 837324a015
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 62 additions and 1 deletions

View file

@ -108,6 +108,8 @@ export const auditLogEventTitle: Record<string, Optional<string>> & {
'Create IdP-initiated SAML SSO authentication session',
'JwtCustomizer.AccessToken': 'Get custom user access token claims',
'JwtCustomizer.ClientCredential': 'Get custom M2M access token claims',
'SamlApplication.AuthnRequest': 'Receive SAML application authentication request',
'SamlApplication.Callback': 'Handle SAML application callback',
});
export const logEventTitle: Record<string, Optional<string>> & {

View file

@ -6,6 +6,7 @@ import { z } from 'zod';
import { spInitiatedSamlSsoSessionCookieName } from '#src/constants/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import koaAuditLog from '#src/middleware/koa-audit-log.js';
import koaGuard from '#src/middleware/koa-guard.js';
import type { AnonymousRouter, RouterInitArgs } from '#src/routes/types.js';
import assertThat from '#src/utils/assert-that.js';
@ -62,12 +63,20 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
query: samlApplicationSignInCallbackQueryParametersGuard,
status: [200, 400],
}),
koaAuditLog(queries),
async (ctx, next) => {
const {
params: { id },
query,
} = ctx.guard;
const log = ctx.createLog('SamlApplication.Callback');
log.append({
query,
samlApplicationId: id,
});
// Handle error in query parameters
if ('error' in query) {
throw new RequestError({
@ -94,6 +103,11 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
const { context, entityEndpoint } = await samlApplication.createSamlResponse(userInfo);
log.append({
context,
entityEndpoint,
});
// Return auto-submit form
ctx.body = generateAutoSubmitForm(entityEndpoint, context);
return next();
@ -115,12 +129,19 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
.catchall(z.string()),
status: [200, 302, 400, 404],
}),
koaAuditLog(queries),
async (ctx, next) => {
const {
params: { id },
query: { Signature, RelayState, ...rest },
} = ctx.guard;
const log = ctx.createLog('SamlApplication.AuthnRequest');
log.append({
query: ctx.guard.query,
samlApplicationId: id,
});
const details = await getSamlApplicationDetailsById(id);
const samlApplication = new SamlApplication(details, id, envSet.oidc.issuer, tenantId);
@ -142,6 +163,7 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
});
const extractResult = authRequestInfoGuard.safeParse(loginRequestResult.extract);
log.append({ extractResult });
if (!extractResult.success) {
throw new RequestError({
@ -150,6 +172,8 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
});
}
log.append({ extractResultData: extractResult.data });
assertThat(
extractResult.data.issuer === samlApplication.details.entityId,
'application.saml.auth_request_issuer_not_match'
@ -182,6 +206,12 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
overwrite: true,
});
log.append({
cookie: {
spInitiatedSamlSsoSessionCookieName: insertSamlAppSession,
},
});
ctx.redirect(signInUrl.toString());
} catch (error: unknown) {
if (error instanceof RequestError) {
@ -208,12 +238,19 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
}),
status: [200, 302, 400, 404],
}),
koaAuditLog(queries),
async (ctx, next) => {
const {
params: { id },
body: { SAMLRequest, RelayState },
} = ctx.guard;
const log = ctx.createLog('SamlApplication.AuthnRequest');
log.append({
body: ctx.guard.body,
samlApplicationId: id,
});
const details = await getSamlApplicationDetailsById(id);
const samlApplication = new SamlApplication(details, id, envSet.oidc.issuer, tenantId);
@ -226,6 +263,7 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
});
const extractResult = authRequestInfoGuard.safeParse(loginRequestResult.extract);
log.append({ extractResult });
if (!extractResult.success) {
throw new RequestError({
@ -233,6 +271,7 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
error: extractResult.error.flatten(),
});
}
log.append({ extractResultData: extractResult.data });
assertThat(
extractResult.data.issuer === samlApplication.details.entityId,
@ -264,6 +303,12 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
overwrite: true,
});
log.append({
cookie: {
spInitiatedSamlSsoSessionCookieName: insertSamlAppSession,
},
});
ctx.redirect(signInUrl.toString());
} catch (error: unknown) {
if (error instanceof RequestError) {

View file

@ -1,19 +1,22 @@
import type * as hook from './hook.js';
import type * as interaction from './interaction.js';
import type * as jwtCustomizer from './jwt-customizer.js';
import type * as saml from './saml.js';
import type * as token from './token.js';
export * as interaction from './interaction.js';
export * as token from './token.js';
export * as hook from './hook.js';
export * as jwtCustomizer from './jwt-customizer.js';
export * as saml from './saml.js';
/** Fallback for empty or unrecognized log keys. */
export const LogKeyUnknown = 'Unknown';
export type AuditLogKey = typeof LogKeyUnknown | interaction.LogKey | token.LogKey;
export type AuditLogKey = typeof LogKeyUnknown | interaction.LogKey | token.LogKey | saml.LogKey;
export type WebhookLogKey = hook.LogKey;
export type JwtCustomizerLogKey = jwtCustomizer.LogKey;
export type SamlLogKey = saml.LogKey;
/**
* The union type of all available log keys.
@ -21,5 +24,6 @@ export type JwtCustomizerLogKey = jwtCustomizer.LogKey;
*
* @see {@link interaction.LogKey} for interaction log keys.
* @see {@link token.LogKey} for token log keys.
* @see {@link saml.LogKey} for SAML application log keys.
**/
export type LogKey = AuditLogKey | WebhookLogKey | JwtCustomizerLogKey;

View file

@ -0,0 +1,10 @@
export type Prefix = 'SamlApplication';
export const prefix: Prefix = 'SamlApplication';
export enum Scenario {
Callback = 'Callback',
AuthnRequest = 'AuthnRequest',
}
export type LogKey = `${Prefix}.${Scenario}`;