feat(auth): mandatory 2FA (TOTP + WebAuthn passkeys) with hard enrollment gate, AAL2 step-up, and single-use recovery codes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user