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

View File

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