This commit is contained in:
Bastian Wagner
2026-07-15 17:08:17 +02:00
parent 426c18ad57
commit d0600ab6ac
9 changed files with 206 additions and 43 deletions

View File

@@ -83,7 +83,7 @@ export class AccountController {
const tokenHash = hashToken(dto.token, this.tokenSecret);
const record = await this.emailChanges.findOne({ where: { tokenHash, consumedAt: IsNull() } });
if (!record || record.expiresAt.getTime() < Date.now() || record.username !== request.user.username) {
return { message: 'Der Bestaetigungslink ist ungueltig oder abgelaufen.' };
return { message: 'Der Bestaetigungslink ist ungültig oder abgelaufen.' };
}
await this.lldap.updateUser(request.user.username, { email: record.newEmail });

View File

@@ -15,7 +15,7 @@ export class AuthService {
const valid = await this.ldapAuth.verifyPassword(username, password);
if (!valid) {
await this.audit.record({ type: 'auth.login_failed', username, ipAddress, userAgent });
throw new UnauthorizedException('Ungueltige Zugangsdaten.');
throw new UnauthorizedException('Ungültige Zugangsdaten.');
}
await this.audit.record({ type: 'auth.login_success', username, ipAddress, userAgent });

View File

@@ -18,7 +18,7 @@ export class JwtAuthGuard implements CanActivate {
request.user = await this.jwt.verifyAsync<RequestUser>(token);
return true;
} catch {
throw new UnauthorizedException('Session ist ungueltig oder abgelaufen.');
throw new UnauthorizedException('Session ist ungültig oder abgelaufen.');
}
}

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Req, Res, UnauthorizedException } from '@nestjs/common';
import { Request, Response } from 'express';
import { OidcProviderService } from './oidc-provider.service';
@@ -10,7 +10,7 @@ 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('Ungueltige Anfrage', '<p>Die OIDC-Interaktion ist ungueltig.</p>'));
response.status(400).send(this.page('Ungültige Anfrage', '<p>Die OIDC-Interaktion ist ungültig.</p>'));
return;
}
@@ -18,16 +18,7 @@ export class OidcInteractionController {
response.send(
this.page(
'Anmelden',
`
<form method="post" action="/interaction/${encodeURIComponent(uid)}/login">
<label>Benutzername <input name="username" autocomplete="username" required></label>
<label>Passwort <input name="password" type="password" autocomplete="current-password" required></label>
<button type="submit">Anmelden</button>
</form>
<form method="post" action="/interaction/${encodeURIComponent(uid)}/abort">
<button class="secondary" type="submit">Abbrechen</button>
</form>
`,
this.loginForm(uid),
),
);
return;
@@ -38,7 +29,7 @@ export class OidcInteractionController {
this.page(
'Zugriff erlauben',
`
<p>Client <strong>${this.escape(String(details.params.client_id ?? ''))}</strong> moechte Zugriff auf folgende Scopes:</p>
<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>
@@ -62,7 +53,19 @@ export class OidcInteractionController {
@Req() request: Request,
@Res() response: Response,
) {
await this.oidc.finishLogin(request, response, uid, body.username ?? '', body.password ?? '');
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, 'Ungültige Zugangsdaten.')));
return;
}
throw error;
}
}
@Post(':uid/confirm')
@@ -75,6 +78,25 @@ export class OidcInteractionController {
await this.oidc.abortInteraction(request, response);
}
private loginForm(uid: string, username = '', errorMessage = ''): string {
const encodedUid = encodeURIComponent(uid);
const error = errorMessage
? `<p class="message error" role="alert">${this.escape(errorMessage)}</p>`
: '';
return `
${error}
<form method="post" action="/interaction/${encodedUid}/login">
<label>Benutzername <input name="username" autocomplete="username" value="${this.escape(username)}" required></label>
<label>Passwort <input name="password" type="password" autocomplete="current-password" required autofocus></label>
<button type="submit">Anmelden</button>
</form>
<form method="post" action="/interaction/${encodedUid}/abort">
<button class="secondary" type="submit">Abbrechen</button>
</form>
`;
}
private page(title: string, body: string): string {
return `<!doctype html>
<html lang="de">
@@ -91,6 +113,8 @@ export class OidcInteractionController {
input { border: 1px solid #bcc8d3; border-radius: 6px; font: inherit; min-height: 44px; padding: 10px 12px; }
button { background: #0f6b6e; border: 1px solid #0f6b6e; border-radius: 6px; color: white; cursor: pointer; font: inherit; font-weight: 700; min-height: 44px; padding: 10px 14px; }
button.secondary { background: white; color: #0f6b6e; }
.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; }
</style>
</head>
@@ -106,4 +130,8 @@ export class OidcInteractionController {
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
private isInvalidCredentialsError(error: unknown): boolean {
return error instanceof UnauthorizedException && error.message === 'Ungültige Zugangsdaten.';
}
}

View File

@@ -63,13 +63,13 @@ export class OidcProviderService implements OnModuleInit {
): Promise<void> {
const details = await this.interactionDetails(request, response);
if (details.uid !== uid || details.prompt.name !== 'login') {
throw new UnauthorizedException('Ungueltige OIDC-Interaktion.');
throw new UnauthorizedException('Ungültige OIDC-Interaktion.');
}
const valid = await this.ldapAuth.verifyPassword(username, password);
if (!valid) {
await this.audit.record({ type: 'oidc.login_failed', username, ipAddress: request.ip, userAgent: request.headers['user-agent'] });
throw new UnauthorizedException('Ungueltige Zugangsdaten.');
throw new UnauthorizedException('Ungültige Zugangsdaten.');
}
const account = await this.lldap.getAccount(username);
@@ -96,7 +96,7 @@ export class OidcProviderService implements OnModuleInit {
async finishConsent(request: Request, response: Response, uid: string): Promise<void> {
const details = await this.interactionDetails(request, response);
if (details.uid !== uid || details.prompt.name !== 'consent') {
throw new UnauthorizedException('Ungueltige OIDC-Interaktion.');
throw new UnauthorizedException('Ungültige OIDC-Interaktion.');
}
const clientId = String(details.params.client_id ?? '');

View File

@@ -86,7 +86,7 @@ export class PasswordService {
const tokenHash = hashToken(token, this.tokenSecret);
const record = await this.resetTokens.findOne({ where: { tokenHash, consumedAt: IsNull() } });
if (!record || record.expiresAt.getTime() < Date.now()) {
throw new BadRequestException('Der Reset-Link ist ungueltig oder abgelaufen.');
throw new BadRequestException('Der Reset-Link ist ungültig oder abgelaufen.');
}
await this.lldap.setPassword(record.username, newPassword);

View File

@@ -75,7 +75,7 @@ export class RegistrationService {
const tokenHash = hashToken(token, this.tokenSecret);
const tokenRecord = await this.emailTokens.findOne({ where: { tokenHash, consumedAt: IsNull() } });
if (!tokenRecord || tokenRecord.expiresAt.getTime() < Date.now()) {
throw new BadRequestException('Der Bestaetigungslink ist ungueltig oder abgelaufen.');
throw new BadRequestException('Der Bestaetigungslink ist ungültig oder abgelaufen.');
}
const registration = await this.registrations.findOneByOrFail({ id: tokenRecord.registrationId });