- 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]>
39 lines
1.3 KiB
TypeScript
39 lines
1.3 KiB
TypeScript
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { Prisma } from '../../generated/prisma';
|
|
import { OneTimeTokenClaims } from './util/jwt-claims';
|
|
|
|
/**
|
|
* Makes a short-lived helper token usable exactly once (SSO completion,
|
|
* MFA challenge after a successful code): its id is recorded, and a
|
|
* second presentation - a replayed request, a copied cookie - is refused.
|
|
* Works across replicas because the record lives in the database.
|
|
*/
|
|
@Injectable()
|
|
export class OneTimeTokenService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async consume(claims: OneTimeTokenClaims): Promise<void> {
|
|
if (!claims.jti || !claims.exp) {
|
|
throw new UnauthorizedException(
|
|
'Anmeldung abgelaufen, bitte erneut anmelden',
|
|
);
|
|
}
|
|
try {
|
|
await this.prisma.consumedToken.create({
|
|
data: { jti: claims.jti, expiresAt: new Date(claims.exp * 1000) },
|
|
});
|
|
} catch (error: unknown) {
|
|
if (
|
|
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
error.code === 'P2002'
|
|
) {
|
|
throw new UnauthorizedException(
|
|
'Diese Anmeldung wurde bereits abgeschlossen, bitte erneut anmelden',
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|