mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(core): add SAML app audit logs (#6931)
This commit is contained in:
parent
d16666a471
commit
837324a015
4 changed files with 62 additions and 1 deletions
|
@ -108,6 +108,8 @@ export const auditLogEventTitle: Record<string, Optional<string>> & {
|
||||||
'Create IdP-initiated SAML SSO authentication session',
|
'Create IdP-initiated SAML SSO authentication session',
|
||||||
'JwtCustomizer.AccessToken': 'Get custom user access token claims',
|
'JwtCustomizer.AccessToken': 'Get custom user access token claims',
|
||||||
'JwtCustomizer.ClientCredential': 'Get custom M2M 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>> & {
|
export const logEventTitle: Record<string, Optional<string>> & {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import { spInitiatedSamlSsoSessionCookieName } from '#src/constants/index.js';
|
import { spInitiatedSamlSsoSessionCookieName } from '#src/constants/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/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 koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import type { AnonymousRouter, RouterInitArgs } from '#src/routes/types.js';
|
import type { AnonymousRouter, RouterInitArgs } from '#src/routes/types.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
@ -62,12 +63,20 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
query: samlApplicationSignInCallbackQueryParametersGuard,
|
query: samlApplicationSignInCallbackQueryParametersGuard,
|
||||||
status: [200, 400],
|
status: [200, 400],
|
||||||
}),
|
}),
|
||||||
|
koaAuditLog(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
params: { id },
|
params: { id },
|
||||||
query,
|
query,
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
|
const log = ctx.createLog('SamlApplication.Callback');
|
||||||
|
|
||||||
|
log.append({
|
||||||
|
query,
|
||||||
|
samlApplicationId: id,
|
||||||
|
});
|
||||||
|
|
||||||
// Handle error in query parameters
|
// Handle error in query parameters
|
||||||
if ('error' in query) {
|
if ('error' in query) {
|
||||||
throw new RequestError({
|
throw new RequestError({
|
||||||
|
@ -94,6 +103,11 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
|
|
||||||
const { context, entityEndpoint } = await samlApplication.createSamlResponse(userInfo);
|
const { context, entityEndpoint } = await samlApplication.createSamlResponse(userInfo);
|
||||||
|
|
||||||
|
log.append({
|
||||||
|
context,
|
||||||
|
entityEndpoint,
|
||||||
|
});
|
||||||
|
|
||||||
// Return auto-submit form
|
// Return auto-submit form
|
||||||
ctx.body = generateAutoSubmitForm(entityEndpoint, context);
|
ctx.body = generateAutoSubmitForm(entityEndpoint, context);
|
||||||
return next();
|
return next();
|
||||||
|
@ -115,12 +129,19 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
.catchall(z.string()),
|
.catchall(z.string()),
|
||||||
status: [200, 302, 400, 404],
|
status: [200, 302, 400, 404],
|
||||||
}),
|
}),
|
||||||
|
koaAuditLog(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
params: { id },
|
params: { id },
|
||||||
query: { Signature, RelayState, ...rest },
|
query: { Signature, RelayState, ...rest },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
|
const log = ctx.createLog('SamlApplication.AuthnRequest');
|
||||||
|
log.append({
|
||||||
|
query: ctx.guard.query,
|
||||||
|
samlApplicationId: id,
|
||||||
|
});
|
||||||
|
|
||||||
const details = await getSamlApplicationDetailsById(id);
|
const details = await getSamlApplicationDetailsById(id);
|
||||||
const samlApplication = new SamlApplication(details, id, envSet.oidc.issuer, tenantId);
|
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);
|
const extractResult = authRequestInfoGuard.safeParse(loginRequestResult.extract);
|
||||||
|
log.append({ extractResult });
|
||||||
|
|
||||||
if (!extractResult.success) {
|
if (!extractResult.success) {
|
||||||
throw new RequestError({
|
throw new RequestError({
|
||||||
|
@ -150,6 +172,8 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.append({ extractResultData: extractResult.data });
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
extractResult.data.issuer === samlApplication.details.entityId,
|
extractResult.data.issuer === samlApplication.details.entityId,
|
||||||
'application.saml.auth_request_issuer_not_match'
|
'application.saml.auth_request_issuer_not_match'
|
||||||
|
@ -182,6 +206,12 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.append({
|
||||||
|
cookie: {
|
||||||
|
spInitiatedSamlSsoSessionCookieName: insertSamlAppSession,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
ctx.redirect(signInUrl.toString());
|
ctx.redirect(signInUrl.toString());
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
|
@ -208,12 +238,19 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
}),
|
}),
|
||||||
status: [200, 302, 400, 404],
|
status: [200, 302, 400, 404],
|
||||||
}),
|
}),
|
||||||
|
koaAuditLog(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
params: { id },
|
params: { id },
|
||||||
body: { SAMLRequest, RelayState },
|
body: { SAMLRequest, RelayState },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
|
const log = ctx.createLog('SamlApplication.AuthnRequest');
|
||||||
|
log.append({
|
||||||
|
body: ctx.guard.body,
|
||||||
|
samlApplicationId: id,
|
||||||
|
});
|
||||||
|
|
||||||
const details = await getSamlApplicationDetailsById(id);
|
const details = await getSamlApplicationDetailsById(id);
|
||||||
const samlApplication = new SamlApplication(details, id, envSet.oidc.issuer, tenantId);
|
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);
|
const extractResult = authRequestInfoGuard.safeParse(loginRequestResult.extract);
|
||||||
|
log.append({ extractResult });
|
||||||
|
|
||||||
if (!extractResult.success) {
|
if (!extractResult.success) {
|
||||||
throw new RequestError({
|
throw new RequestError({
|
||||||
|
@ -233,6 +271,7 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
error: extractResult.error.flatten(),
|
error: extractResult.error.flatten(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
log.append({ extractResultData: extractResult.data });
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
extractResult.data.issuer === samlApplication.details.entityId,
|
extractResult.data.issuer === samlApplication.details.entityId,
|
||||||
|
@ -264,6 +303,12 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.append({
|
||||||
|
cookie: {
|
||||||
|
spInitiatedSamlSsoSessionCookieName: insertSamlAppSession,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
ctx.redirect(signInUrl.toString());
|
ctx.redirect(signInUrl.toString());
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import type * as hook from './hook.js';
|
import type * as hook from './hook.js';
|
||||||
import type * as interaction from './interaction.js';
|
import type * as interaction from './interaction.js';
|
||||||
import type * as jwtCustomizer from './jwt-customizer.js';
|
import type * as jwtCustomizer from './jwt-customizer.js';
|
||||||
|
import type * as saml from './saml.js';
|
||||||
import type * as token from './token.js';
|
import type * as token from './token.js';
|
||||||
|
|
||||||
export * as interaction from './interaction.js';
|
export * as interaction from './interaction.js';
|
||||||
export * as token from './token.js';
|
export * as token from './token.js';
|
||||||
export * as hook from './hook.js';
|
export * as hook from './hook.js';
|
||||||
export * as jwtCustomizer from './jwt-customizer.js';
|
export * as jwtCustomizer from './jwt-customizer.js';
|
||||||
|
export * as saml from './saml.js';
|
||||||
|
|
||||||
/** Fallback for empty or unrecognized log keys. */
|
/** Fallback for empty or unrecognized log keys. */
|
||||||
export const LogKeyUnknown = 'Unknown';
|
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 WebhookLogKey = hook.LogKey;
|
||||||
export type JwtCustomizerLogKey = jwtCustomizer.LogKey;
|
export type JwtCustomizerLogKey = jwtCustomizer.LogKey;
|
||||||
|
export type SamlLogKey = saml.LogKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The union type of all available log keys.
|
* 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 interaction.LogKey} for interaction log keys.
|
||||||
* @see {@link token.LogKey} for token 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;
|
export type LogKey = AuditLogKey | WebhookLogKey | JwtCustomizerLogKey;
|
||||||
|
|
10
packages/schemas/src/types/log/saml.ts
Normal file
10
packages/schemas/src/types/log/saml.ts
Normal 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}`;
|
Loading…
Add table
Reference in a new issue