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:
1 parent
fc6af8737b
commit
45efb08369
16 files changed
+166
-23
No files matched your search
@@ -8,6 +8,7 @@ import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { PermissionResolverService } from './permissions/permission-resolver.service';
|
||||
import { MfaService } from './mfa/mfa.service';
|
||||
import { AccountGuardService } from './mfa/account-guard.service';
|
||||
import { OneTimeTokenService } from './one-time-token.service';
|
||||
import { parseDurationToMs } from './util/duration';
|
||||
import { TOKEN_AUDIENCE, jwtSignOptions } from './util/jwt-claims';
|
||||
|
||||
@@ -64,6 +65,7 @@ export class AuthService {
|
||||
private readonly permissionResolver: PermissionResolverService,
|
||||
private readonly mfaService: MfaService,
|
||||
private readonly accountGuard: AccountGuardService,
|
||||
private readonly oneTimeTokens: OneTimeTokenService,
|
||||
) {}
|
||||
|
||||
async getCurrentUserProfile(userId: string): Promise<CurrentUserProfile> {
|
||||
@@ -208,7 +210,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verifyMfaAndLogin(mfaToken: string, code: string): Promise<AuthTokens> {
|
||||
const { userId } = this.mfaService.verifyChallengeToken(mfaToken);
|
||||
const { userId, claims } = this.mfaService.verifyChallengeToken(mfaToken);
|
||||
const user = await this.usersService.findById(userId);
|
||||
if (!user.isActive || !user.mfaEnabled || !user.mfaSecret) {
|
||||
throw new UnauthorizedException('MFA nicht verfügbar');
|
||||
@@ -230,6 +232,8 @@ export class AuthService {
|
||||
});
|
||||
throw new UnauthorizedException('Code ungültig');
|
||||
}
|
||||
// A challenge that already opened a session cannot open another one.
|
||||
await this.oneTimeTokens.consume(claims);
|
||||
await this.accountGuard.recordSuccess(user.id);
|
||||
const authenticatedUser = {
|
||||
id: user.id,
|
||||
|
||||
Reference in new issue
Block a user