fix: correct Experience API endpoints for TOTP and backup codes
All checks were successful
CI / build (push) Successful in 2m4s
CI / docker (push) Successful in 1m1s

- TOTP secret: /verification/totp/secret (not /verification/totp)
- Backup codes: generate via /verification/backup-code/generate first,
  then bind with the returned verificationId. Cannot bind BackupCode
  without generating codes first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-27 19:30:54 +02:00
parent d8f7452ab7
commit 040ae60be5
2 changed files with 17 additions and 8 deletions

View File

@@ -9,7 +9,7 @@ import {
verifyTotp, verifyBackupCode, submitMfa,
startWebAuthnAuth, verifyWebAuthnAuth,
startWebAuthnRegistration, verifyWebAuthnRegistration, bindMfaProfile,
createTotpSecret, verifyTotpSetup,
generateBackupCodes, createTotpSecret, verifyTotpSetup,
skipMfaEnrollment, submitInteraction,
MfaRequiredError, MfaEnrollmentError,
} from './experience-api';
@@ -318,7 +318,8 @@ export function SignInPage() {
const credential = await startWebAuthnReg({ optionsJSON: registrationOptions as any });
const verifiedId = await verifyWebAuthnRegistration(verificationId, credential as unknown as Record<string, unknown>);
await bindMfaProfile('WebAuthn', verifiedId);
await bindMfaProfile('BackupCode');
const bc = await generateBackupCodes();
await bindMfaProfile('BackupCode', bc.verificationId);
const result = await submitInteraction();
window.location.replace(result);
} catch (err) {
@@ -353,7 +354,8 @@ export function SignInPage() {
try {
const verifiedId = await verifyTotpSetup(totpCode);
await bindMfaProfile('Totp', verifiedId);
await bindMfaProfile('BackupCode');
const bc = await generateBackupCodes();
await bindMfaProfile('BackupCode', bc.verificationId);
const result = await submitInteraction();
window.location.replace(result);
} catch (err) {

View File

@@ -322,18 +322,25 @@ export async function verifyWebAuthnRegistration(verificationId: string, payload
return data.verificationId;
}
export async function bindMfaProfile(type: string, verificationId?: string): Promise<void> {
const body: Record<string, string> = { type };
if (verificationId) body.verificationId = verificationId;
const res = await request('POST', '/profile/mfa', body);
export async function bindMfaProfile(type: string, verificationId: string): Promise<void> {
const res = await request('POST', '/profile/mfa', { type, verificationId });
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `Failed to bind MFA (${res.status})`);
}
}
export async function generateBackupCodes(): Promise<{ verificationId: string; codes: string[] }> {
const res = await request('POST', '/verification/backup-code/generate');
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `Failed to generate backup codes (${res.status})`);
}
return res.json();
}
export async function createTotpSecret(): Promise<{ secret: string; secretQrCode: string; verificationId: string }> {
const res = await request('POST', '/verification/totp');
const res = await request('POST', '/verification/totp/secret');
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `Failed to create TOTP secret (${res.status})`);