diff --git a/apps/api/src/application-error-log/application-error-codes.ts b/apps/api/src/application-error-log/application-error-codes.ts index cdb3847..945445a 100644 --- a/apps/api/src/application-error-log/application-error-codes.ts +++ b/apps/api/src/application-error-log/application-error-codes.ts @@ -16,6 +16,7 @@ export enum ApplicationErrorCode { EXTERNAL_API_REQUEST_FAILED = 'EXTERNAL_API_REQUEST_FAILED', FILE_GENERATION_FAILED = 'FILE_GENERATION_FAILED', OIDC_AUTHORIZATION_ERROR = 'OIDC_AUTHORIZATION_ERROR', + OIDC_INTERACTION_SESSION_NOT_FOUND = 'OIDC_INTERACTION_SESSION_NOT_FOUND', OIDC_PROVIDER_ERROR = 'OIDC_PROVIDER_ERROR', PASSWORD_RESET_EMAIL_SEND_FAILED = 'PASSWORD_RESET_EMAIL_SEND_FAILED', REGISTRATION_APPROVAL_NOTIFICATION_FAILED = 'REGISTRATION_APPROVAL_NOTIFICATION_FAILED', diff --git a/apps/api/src/oidc/oidc-interaction.controller.ts b/apps/api/src/oidc/oidc-interaction.controller.ts index 9a192d1..cc0caae 100644 --- a/apps/api/src/oidc/oidc-interaction.controller.ts +++ b/apps/api/src/oidc/oidc-interaction.controller.ts @@ -2,6 +2,9 @@ import { Body, Controller, Get, Param, Post, Req, Res, UnauthorizedException } f import { ConfigService } from '@nestjs/config'; import { Request, Response } from 'express'; import type { Interaction } from 'oidc-provider'; +import { ApplicationErrorCategory, ApplicationErrorCode } from '../application-error-log/application-error-codes'; +import { ApplicationErrorLoggerService } from '../application-error-log/application-error-logger.service'; +import { RequestContextService } from '../common/request-context.service'; import { OidcProviderService } from './oidc-provider.service'; @Controller('interaction') @@ -9,11 +12,28 @@ export class OidcInteractionController { constructor( private readonly oidc: OidcProviderService, private readonly config: ConfigService, + private readonly applicationErrorLogger: ApplicationErrorLoggerService, + private readonly requestContext: RequestContextService, ) {} @Get(':uid') async view(@Param('uid') uid: string, @Req() request: Request, @Res() response: Response) { - const details = await this.oidc.interactionDetails(request, response); + let details: Interaction; + try { + details = await this.oidc.interactionDetails(request, response); + } catch (error) { + await this.logInteractionSessionError(error, uid, request); + response + .status(400) + .send( + this.page( + 'Anmeldung abgelaufen', + '', + ), + ); + return; + } + if (details.uid !== uid) { response.status(400).send(this.page('Ungueltige Anfrage', '

Die OIDC-Interaktion ist ungueltig.

')); return; @@ -194,4 +214,33 @@ export class OidcInteractionController { private isInvalidCredentialsError(error: unknown): boolean { return error instanceof UnauthorizedException && error.message === 'Ungueltige Zugangsdaten.'; } + + private async logInteractionSessionError(error: unknown, uid: string, request: Request): Promise { + const cookieHeader = request.headers.cookie ?? ''; + await this.applicationErrorLogger.log({ + error, + category: ApplicationErrorCategory.OIDC, + code: ApplicationErrorCode.OIDC_INTERACTION_SESSION_NOT_FOUND, + module: 'OidcModule', + service: OidcInteractionController.name, + operation: 'viewInteraction', + requestContext: { + ...this.requestContext.get(), + method: request.method, + path: request.originalUrl || request.url, + statusCode: 400, + }, + context: { + uid, + hasCookieHeader: Boolean(cookieHeader), + hasInteractionCookie: /(?:^|;\s*)_interaction=/.test(cookieHeader), + forwardedProto: request.headers['x-forwarded-proto'], + forwardedHost: request.headers['x-forwarded-host'], + host: request.headers.host, + referer: request.headers.referer, + userAgent: request.headers['user-agent'], + }, + handled: true, + }); + } } diff --git a/apps/api/src/oidc/oidc-provider.service.ts b/apps/api/src/oidc/oidc-provider.service.ts index f7e1da4..6d83613 100644 --- a/apps/api/src/oidc/oidc-provider.service.ts +++ b/apps/api/src/oidc/oidc-provider.service.ts @@ -285,6 +285,27 @@ export class OidcProviderService implements OnModuleInit { provider.on('access_token.issued', (token) => { void this.audit.record({ type: 'oidc.access_token_issued', username: token.accountId, metadata: { clientId: token.clientId } }); }); + provider.on('interaction.started', (ctx, prompt) => { + const oidcContext = ctx as { + path?: string; + oidc?: { + client?: { clientId?: string }; + params?: Record; + entities?: { Interaction?: { uid?: string } }; + }; + }; + void this.audit.record({ + type: 'oidc.interaction_started', + metadata: { + prompt: typeof prompt === 'object' && prompt && 'name' in prompt ? String(prompt.name) : undefined, + path: oidcContext.path, + clientId: oidcContext.oidc?.client?.clientId ?? oidcContext.oidc?.params?.client_id, + redirectUri: oidcContext.oidc?.params?.redirect_uri, + scope: oidcContext.oidc?.params?.scope, + interactionUid: oidcContext.oidc?.entities?.Interaction?.uid, + }, + }); + }); } private registerErrorEvents(provider: Provider): void {