fix(auth): one-time SSO and MFA tokens, permission changes effective on every replica

- Every JWT carries a jti. The SSO completion token and the MFA challenge
  (once a code was accepted) are recorded as used in ConsumedToken; a
  replayed request or copied cookie cannot open a second session. Used
  ids are cleaned up after they expire.
- The token-validity check (active user, active role, password change)
  reads fresh on every request and refreshes the permission cache, so a
  deactivation or role change made on another replica applies to the very
  next request instead of up to 30 s later.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 07:24:28 +02:00
1 parent fc6af8737b
commit 45efb08369
16 files changed
+166 -23

No files matched your search

+13 -7
View File
@@ -9,6 +9,7 @@ import {
verifyTotpStep,
} from './totp.util';
import {
OneTimeTokenClaims,
TOKEN_AUDIENCE,
jwtSignOptions,
jwtVerifyOptions,
@@ -91,13 +92,15 @@ export class MfaService {
);
}
verifyChallengeToken(challengeToken: string): { userId: string } {
let payload: ChallengeTokenPayload;
verifyChallengeToken(challengeToken: string): {
userId: string;
claims: OneTimeTokenClaims;
} {
let payload: ChallengeTokenPayload & OneTimeTokenClaims;
try {
payload = this.jwtService.verify<ChallengeTokenPayload>(
challengeToken,
jwtVerifyOptions(TOKEN_AUDIENCE.mfaChallenge),
);
payload = this.jwtService.verify<
ChallengeTokenPayload & OneTimeTokenClaims
>(challengeToken, jwtVerifyOptions(TOKEN_AUDIENCE.mfaChallenge));
} catch {
throw new UnauthorizedException(
'Anmeldung abgelaufen, bitte erneut mit E-Mail und Passwort anmelden',
@@ -106,7 +109,10 @@ export class MfaService {
if (payload.purpose !== CHALLENGE_TOKEN_PURPOSE) {
throw new UnauthorizedException('Ungültiger Token');
}
return { userId: payload.sub };
return {
userId: payload.sub,
claims: { jti: payload.jti, exp: payload.exp },
};
}
/** See verifyTotpStep; use AccountGuardService.consumeTotp for logins. */