From 76a62135ab2afb42960e885ca704a144bf30a779 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:55:16 +0200 Subject: [PATCH] feat: add WebAuthn and method picker modes to sign-in UI Adds mfaWebauthn and mfaMethodPicker modes with smart routing based on stored preference (localStorage). Auto-triggers passkey prompt on mode entry. Adds "Use passkey instead" link in TOTP mode. Saves method preference on successful verification. Co-Authored-By: Claude Sonnet 4.6 --- ui/sign-in/src/SignInPage.tsx | 87 ++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/ui/sign-in/src/SignInPage.tsx b/ui/sign-in/src/SignInPage.tsx index 50cf923..07caece 100644 --- a/ui/sign-in/src/SignInPage.tsx +++ b/ui/sign-in/src/SignInPage.tsx @@ -2,15 +2,17 @@ import { type FormEvent, useEffect, useMemo, useState } from 'react'; import { Eye, EyeOff } from 'lucide-react'; import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system'; import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg'; +import { startAuthentication } from '@simplewebauthn/browser'; import { signIn, startRegistration, completeRegistration, startForgotPassword, forgotPasswordVerifyAndReset, verifyTotp, verifyBackupCode, submitMfa, + startWebAuthnAuth, verifyWebAuthnAuth, MfaRequiredError, } from './experience-api'; import styles from './SignInPage.module.css'; -type Mode = 'signIn' | 'register' | 'verifyCode' | 'forgotPassword' | 'forgotPasswordVerify' | 'mfaVerify' | 'mfaBackupCode'; +type Mode = 'signIn' | 'register' | 'verifyCode' | 'forgotPassword' | 'forgotPasswordVerify' | 'mfaVerify' | 'mfaBackupCode' | 'mfaWebauthn' | 'mfaMethodPicker'; const SIGN_IN_SUBTITLES = [ "Prove you're not a mirage", @@ -83,6 +85,8 @@ export function SignInPage() { const [verificationId, setVerificationId] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmNewPassword, setConfirmNewPassword] = useState(''); + const [webauthnError, setWebauthnError] = useState(''); + const [webauthnLoading, setWebauthnLoading] = useState(false); // Fetch sign-in experience to check if registration is enabled useEffect(() => { @@ -122,7 +126,14 @@ export function SignInPage() { window.location.replace(redirectTo); } catch (err) { if (err instanceof MfaRequiredError) { - setMode('mfaVerify'); + const pref = localStorage.getItem('mfa_method_preference'); + if (pref === 'webauthn') { + setMode('mfaWebauthn'); + } else if (pref === 'totp') { + setMode('mfaVerify'); + } else { + setMode('mfaMethodPicker'); + } setLoading(false); return; } @@ -233,6 +244,7 @@ export function SignInPage() { setLoading(true); try { const verificationId = await verifyTotp(code); + localStorage.setItem('mfa_method_preference', 'totp'); const redirectTo = await submitMfa(verificationId); window.location.replace(redirectTo); } catch (err) { @@ -241,6 +253,31 @@ export function SignInPage() { } }; + // --- MFA: WebAuthn/passkey verification --- + async function handleWebAuthnVerify() { + setWebauthnError(''); + setWebauthnLoading(true); + try { + const options = await startWebAuthnAuth(); + const credential = await startAuthentication({ optionsJSON: options as any }); + const verificationId = await verifyWebAuthnAuth(credential as unknown as Record); + localStorage.setItem('mfa_method_preference', 'webauthn'); + const redirectTo = await submitMfa(verificationId); + window.location.replace(redirectTo); + } catch (err) { + setWebauthnError(err instanceof Error ? err.message : 'Passkey verification failed'); + setWebauthnLoading(false); + } + } + + // Auto-trigger passkey prompt when entering mfaWebauthn mode + useEffect(() => { + if (mode === 'mfaWebauthn') { + handleWebAuthnVerify(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]); + // --- MFA: backup code verification --- const handleBackupCodeVerify = async (e: FormEvent) => { e.preventDefault(); @@ -589,6 +626,13 @@ export function SignInPage() { > Use a backup code + )} @@ -630,6 +674,45 @@ export function SignInPage() {

)} + + {/* --- MFA: method picker --- */} + {mode === 'mfaMethodPicker' && ( +
+
+

Verify your identity

+

Choose a verification method

+
+
+ + +
+
+ )} + + {/* --- MFA: WebAuthn/passkey verification --- */} + {mode === 'mfaWebauthn' && ( +
+
+

Passkey verification

+

+ Use your fingerprint, face, or security key +

+
+ {webauthnError && {webauthnError}} + +
+ +
+
+ )}