From 040ae60be5f48230012d8a50cf70cfd18120ff28 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:30:54 +0200 Subject: [PATCH] fix: correct Experience API endpoints for TOTP and backup codes - 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) --- ui/sign-in/src/SignInPage.tsx | 8 +++++--- ui/sign-in/src/experience-api.ts | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ui/sign-in/src/SignInPage.tsx b/ui/sign-in/src/SignInPage.tsx index a610227..5429d07 100644 --- a/ui/sign-in/src/SignInPage.tsx +++ b/ui/sign-in/src/SignInPage.tsx @@ -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); 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) { diff --git a/ui/sign-in/src/experience-api.ts b/ui/sign-in/src/experience-api.ts index 30709ef..437721e 100644 --- a/ui/sign-in/src/experience-api.ts +++ b/ui/sign-in/src/experience-api.ts @@ -322,18 +322,25 @@ export async function verifyWebAuthnRegistration(verificationId: string, payload return data.verificationId; } -export async function bindMfaProfile(type: string, verificationId?: string): Promise { - const body: Record = { type }; - if (verificationId) body.verificationId = verificationId; - const res = await request('POST', '/profile/mfa', body); +export async function bindMfaProfile(type: string, verificationId: string): Promise { + 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})`);