feat(auth): mandatory 2FA (TOTP + WebAuthn passkeys) with hard enrollment gate, AAL2 step-up, and single-use recovery codes

This commit is contained in:
Gerhard Scheikl
2026-05-31 21:38:01 +02:00
parent 129e21529c
commit e14e909700
19 changed files with 1310 additions and 142 deletions
+31
View File
@@ -0,0 +1,31 @@
import { createHash, randomBytes } from 'crypto';
/**
* MFA recovery codes.
*
* GoTrue has no native recovery-code support, so we manage them ourselves in
* the `mfa_recovery_codes` table (service-role only; see migration 0002). We
* only ever persist a SHA-256 hash of each code — the plaintext is shown to the
* user exactly once at generation time. Codes are high-entropy random values,
* so a fast hash is sufficient (no need for bcrypt/per-code salt).
*/
/** Normalise user input so hyphens / case / spacing don't matter on redeem. */
function normalize(code: string): string {
return code.replace(/[^a-z0-9]/gi, '').toLowerCase();
}
/** Stable hash used both when storing and when redeeming a code. */
export function hashRecoveryCode(code: string): string {
return createHash('sha256').update(normalize(code)).digest('hex');
}
/** Generate `n` formatted recovery codes (e.g. `a1b2-c3d4-e5f6`). */
export function generateRecoveryCodes(n = 10): string[] {
const codes: string[] = [];
for (let i = 0; i < n; i++) {
const raw = randomBytes(6).toString('hex'); // 12 hex chars
codes.push(`${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`);
}
return codes;
}