Skip to main content

Audit events and observability

Every auth decision emits a structured event. The default LoggerAuditSink writes them through NestJS Logger with automatic redaction (JWT-shaped strings, cookies, traits, admin tokens all stripped).

To ship events elsewhere, provide your own sink:

import { Injectable } from '@nestjs/common';
import { AuditSink, AUDIT_SINK, IamAuditEvent } from 'ory-nestjs';

@Injectable()
export class OtelAuditSink implements AuditSink {
async emit(event: IamAuditEvent) {
// push to OTel log record, SIEM webhook, Kafka, whatever.
}
}

IamModule.forRoot({
tenants: { /* … */ },
auditSink: { provide: AUDIT_SINK, useClass: OtelAuditSink },
});

Every mutating call on every service emits an event. Events follow the stable naming convention iam.<product>.<action> (e.g. iam.identity.patch, iam.jwk.createSet).

Authentication / authorization pipeline:

EventSourceLevel
auth.successSessionGuard, OAuth2Guardinfo
auth.failure.missing_credentialSessionGuardwarn
auth.failure.expiredtransport/mapperwarn
auth.failure.malformedtransportwarn
auth.failure.token_inactiveOAuth2Guardwarn
auth.failure.unsigned_headerOathkeeperTransportwarn
auth.failure.upstreamSessionGuardwarn
auth.tenant_mismatchSessionGuardwarn
authz.role.denyRoleGuardwarn
authz.permission.grantPermissionGuard, PermissionService.grantinfo
authz.permission.denyPermissionGuardwarn
authz.permission.revokePermissionService.revokeinfo
authz.upstream_unavailablePermissionGuardwarn
authz.session.revokeSessionService.revoke, IdentityService.revokeSessioninfo

Kratos admin mutations (v0.5.0+):

EventSourceLevel
iam.identity.createIdentityService.createinfo
iam.identity.updateTraitsIdentityService.updateTraitsinfo
iam.identity.patchIdentityService.patch (includes paths[] + opCount)info
iam.identity.deleteIdentityService.deleteinfo
iam.session.extendIdentityService.extendSessioninfo
iam.flow.logout.browserFlowService.submitBrowserLogoutinfo
iam.flow.logout.nativeFlowService.performNativeLogoutinfo
iam.courier.message.accessCourierService.get({ includeBody: true }) — surveillance event for SIEM alerting on admin reads of recovery/verification bodieswarn

Hydra mutations:

EventSourceLevel
oauth2.client.createOAuth2ClientService.createinfo
oauth2.client.deleteOAuth2ClientService.deleteinfo
iam.jwk.createSet / updateSet / deleteSetJwkServiceinfo
iam.jwk.updateKey / deleteKeyJwkServiceinfo
iam.oauth2.trustedIssuer.trustTrustedIssuerService.trustinfo
iam.oauth2.trustedIssuer.deleteTrustedIssuerService.deleteinfo

Ory Network mutations:

EventSourceLevel
iam.network.project.create / setProjectAdminServiceinfo
iam.network.project.purge (attribute irreversible: true)ProjectAdminService.purgeinfo
iam.network.project.apiKey.create / apiKey.deleteProjectAdminServiceinfo
iam.network.workspace.create / updateWorkspaceAdminServiceinfo
iam.network.workspace.apiKey.create / apiKey.deleteWorkspaceAdminServiceinfo
iam.network.events.create / set / deleteEventsServiceinfo

Operational:

EventSourceLevel
health.probe_failureIamHealthIndicatorwarn
config.boot_failureIamModuleerror

:::tip Alerting Any event ending in .purge, .delete, or carrying attributes.irreversible === true is a reasonable default for a SIEM high-severity rule. iam.courier.message.access is specifically emitted so compliance can alert on administrators reading recovery tokens. :::

Health indicator (@nestjs/terminus)

import { TerminusModule, HealthCheckService } from '@nestjs/terminus';
import { IamHealthIndicator } from 'ory-nestjs';

@Controller('health')
export class HealthController {
constructor(private readonly health: HealthCheckService, private readonly iam: IamHealthIndicator) {}

@Get()
@Public()
check() {
return this.health.check([() => this.iam.isHealthy('ory-nestjs')]);
}
}

Probes every configured tenant × product (/health/ready) with a 500ms timeout. Failure payload names the failing tenant + product only — no URLs, tokens, or project slugs leak.

Correlation IDs

SessionGuard reads X-Request-Id off the request (or generates one), stamps it onto outbound Ory calls, and includes it on every audit event. Add your own AsyncLocalStorage-aware logger and requests across the stack will join neatly.