This commit is contained in:
Bastian Wagner
2026-07-15 17:59:25 +02:00
parent 7a6e08d2c5
commit ba7029ac7b
10 changed files with 469 additions and 76 deletions

View File

@@ -1,6 +1,7 @@
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')
@@ -14,38 +15,23 @@ export class OidcInteractionController {
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('Ungültige Anfrage', '<p>Die OIDC-Interaktion ist ungültig.</p>'));
response.status(400).send(this.page('Ungueltige Anfrage', '<p>Die OIDC-Interaktion ist ungueltig.</p>'));
return;
}
if (details.prompt.name === 'login') {
response.send(
this.page(
'Anmelden',
this.loginForm(uid),
),
);
response.send(this.page('Anmelden', this.loginForm(uid)));
return;
}
if (details.prompt.name === 'consent') {
console.log(details)
response.send(
this.page(
'Zugriff erlauben',
`
<p>Client <strong>${this.escape(String(details.params.name ?? ''))}</strong> möchte Zugriff auf folgende Scopes:</p>
<p class="scopes">${this.escape(String(details.params.scope ?? 'openid'))}</p>
<form method="post" action="/interaction/${encodeURIComponent(uid)}/confirm">
<button type="submit">Erlauben</button>
</form>
<form method="post" action="/interaction/${encodeURIComponent(uid)}/abort">
<button class="secondary" type="submit">Ablehnen</button>
</form>
`,
),
);
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;
}
@@ -66,7 +52,7 @@ export class OidcInteractionController {
if (this.isInvalidCredentialsError(error)) {
response
.status(401)
.send(this.page('Anmelden', this.loginForm(uid, username, 'Ungültige Zugangsdaten.')));
.send(this.page('Anmelden', this.loginForm(uid, username, 'Ungueltige Zugangsdaten.')));
return;
}
@@ -104,6 +90,58 @@ export class OidcInteractionController {
`;
}
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 `
<p class="intro">Die Anwendung <strong>${this.escape(clientName)}</strong> moechte auf dein Konto zugreifen.</p>
${redirectUri ? `<dl class="consent-details"><div><dt>Weiterleitung</dt><dd>${this.escape(redirectUri)}</dd></div></dl>` : ''}
<div class="scope-list" aria-label="Angeforderte Berechtigungen">
${this.scopeItems(scope)}
</div>
<form method="post" action="/interaction/${encodedUid}/confirm">
<button type="submit">Zugriff erlauben</button>
</form>
<form method="post" action="/interaction/${encodedUid}/abort">
<button class="secondary" type="submit">Ablehnen</button>
</form>
`;
}
private scopeItems(scope: string): string {
const scopes = scope
.split(/\s+/)
.map((item) => item.trim())
.filter(Boolean);
return scopes
.map(
(item) => `
<section class="scope-item">
<strong>${this.escape(item)}</strong>
<span>${this.escape(this.scopeDescription(item))}</span>
</section>
`,
)
.join('');
}
private scopeDescription(scope: string): string {
const descriptions: Record<string, string> = {
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 `<!doctype html>
<html lang="de">
@@ -113,7 +151,7 @@ export class OidcInteractionController {
<title>${this.escape(title)} - LDAP Portal</title>
<style>
body { background: #f5f7f9; color: #18202a; font-family: Inter, system-ui, sans-serif; margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 20px; }
main { background: white; border: 1px solid #d8e0e7; border-radius: 8px; box-shadow: 0 16px 40px rgb(24 32 42 / 8%); max-width: 420px; padding: 28px; width: 100%; }
main { background: white; border: 1px solid #d8e0e7; border-radius: 8px; box-shadow: 0 16px 40px rgb(24 32 42 / 8%); max-width: 460px; padding: 28px; width: 100%; }
h1 { font-size: 1.45rem; margin: 0 0 22px; }
form { display: grid; gap: 16px; margin-top: 16px; }
label { display: grid; gap: 7px; font-weight: 700; }
@@ -123,9 +161,16 @@ export class OidcInteractionController {
a { color: #0f6b6e; font-weight: 700; text-decoration: none; }
a:hover { text-decoration: underline; }
.form-link { margin: 16px 0 0; text-align: center; }
.intro { color: #3a4551; line-height: 1.45; margin: 0 0 16px; }
.message { background: #edf7f4; border: 1px solid #b8ddd3; border-radius: 6px; color: #24564f; margin: 0 0 16px; padding: 12px; }
.message.error { background: #fff1f0; border-color: #efb5ae; color: #8d2b20; }
.scopes { background: #edf2f5; border-radius: 6px; padding: 10px; word-break: break-word; }
.consent-details { display: grid; gap: 10px; margin: 0 0 16px; }
.consent-details div { display: grid; gap: 5px; }
dt { color: #637083; font-size: 0.82rem; font-weight: 700; }
dd { margin: 0; overflow-wrap: anywhere; }
.scope-list { display: grid; gap: 8px; margin: 16px 0; }
.scope-item { border: 1px solid #d8e0e7; border-radius: 6px; display: grid; gap: 4px; padding: 10px 12px; }
.scope-item span { color: #637083; font-size: 0.9rem; line-height: 1.35; }
</style>
</head>
<body><main><h1>${this.escape(title)}</h1>${body}</main></body>
@@ -147,6 +192,6 @@ export class OidcInteractionController {
}
private isInvalidCredentialsError(error: unknown): boolean {
return error instanceof UnauthorizedException && error.message === 'Ungültige Zugangsdaten.';
return error instanceof UnauthorizedException && error.message === 'Ungueltige Zugangsdaten.';
}
}