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}}
+
+
+
+
+
+ )}