import { Body, Controller, Get, Param, Post, Req, Res, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Request, Response } from 'express'; import type { Interaction } from 'oidc-provider'; import { OidcProviderService } from './oidc-provider.service'; @Controller('interaction') export class OidcInteractionController { constructor( private readonly oidc: OidcProviderService, private readonly config: ConfigService, ) {} @Get(':uid') async view(@Param('uid') uid: string, @Req() request: Request, @Res() response: Response) { const details = await this.oidc.interactionDetails(request, response); if (details.uid !== uid) { response.status(400).send(this.page('Ungueltige Anfrage', '

Die OIDC-Interaktion ist ungueltig.

')); return; } if (details.prompt.name === 'login') { response.send(this.page('Anmelden', this.loginForm(uid))); return; } if (details.prompt.name === 'consent') { const clientId = String(details.params.client_id ?? ''); if (clientId && (await this.oidc.isFirstPartyClient(clientId))) { await this.oidc.finishConsent(request, response, uid, { autoGranted: true }); return; } response.send(this.page('Zugriff erlauben', this.consentView(uid, details))); return; } response.status(400).send(this.page('OIDC', '

Diese Interaktion wird noch nicht unterstuetzt.

')); } @Post(':uid/login') async login( @Param('uid') uid: string, @Body() body: { username?: string; password?: string }, @Req() request: Request, @Res() response: Response, ) { const username = body.username ?? ''; try { await this.oidc.finishLogin(request, response, uid, username, body.password ?? ''); } catch (error) { if (this.isInvalidCredentialsError(error)) { response .status(401) .send(this.page('Anmelden', this.loginForm(uid, username, 'Ungueltige Zugangsdaten.'))); return; } throw error; } } @Post(':uid/confirm') async confirm(@Param('uid') uid: string, @Req() request: Request, @Res() response: Response) { await this.oidc.finishConsent(request, response, uid); } @Post(':uid/abort') async abort(@Req() request: Request, @Res() response: Response) { await this.oidc.abortInteraction(request, response); } private loginForm(uid: string, username = '', errorMessage = ''): string { const encodedUid = encodeURIComponent(uid); const error = errorMessage ? `` : ''; return ` ${error}
`; } private consentView(uid: string, details: Interaction): string { const encodedUid = encodeURIComponent(uid); const clientId = String(details.params.client_id ?? ''); const clientName = String(details.params.name ?? (clientId || 'Unbekannte Anwendung')); const redirectUri = String(details.params.redirect_uri ?? ''); const scope = String(details.params.scope ?? 'openid'); return `

Die Anwendung ${this.escape(clientName)} moechte auf dein Konto zugreifen.

${redirectUri ? `` : ''}
${this.scopeItems(scope)}
`; } private scopeItems(scope: string): string { const scopes = scope .split(/\s+/) .map((item) => item.trim()) .filter(Boolean); return scopes .map( (item) => `
${this.escape(item)} ${this.escape(this.scopeDescription(item))}
`, ) .join(''); } private scopeDescription(scope: string): string { const descriptions: Record = { openid: 'Anmeldung per OpenID Connect bestaetigen.', profile: 'Profilinformationen wie Name und Anzeigename lesen.', email: 'E-Mail-Adresse lesen.', groups: 'Gruppenmitgliedschaften lesen.', offline_access: 'Laengerfristigen Zugriff ueber Refresh Tokens erlauben.', }; return descriptions[scope] ?? 'Diese Berechtigung wurde von der Anwendung angefordert.'; } private page(title: string, body: string): string { return ` ${this.escape(title)} - LDAP Portal

${this.escape(title)}

${body}
`; } private escape(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } private get registrationUrl(): string { const publicWebUrl = this.config.get('PUBLIC_WEB_URL') ?? 'http://localhost:4200'; return `${publicWebUrl.replace(/\/+$/, '')}/register`; } private isInvalidCredentialsError(error: unknown): boolean { return error instanceof UnauthorizedException && error.message === 'Ungueltige Zugangsdaten.'; } }