fix: add password re-verification before passkey registration
Logto Account API requires identity verification (logto-verification-id header) for sensitive MFA operations. Adds a password prompt modal before the WebAuthn ceremony — verifies password first, then proceeds with passkey registration using the verification record ID. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,9 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
FormField,
|
||||
Input,
|
||||
Modal,
|
||||
useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import {
|
||||
@@ -51,6 +53,9 @@ export function PasskeySection() {
|
||||
const [editName, setEditName] = useState('');
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [regPassword, setRegPassword] = useState('');
|
||||
const [regError, setRegError] = useState<string | null>(null);
|
||||
|
||||
function parseAgent(agent: string | null): string {
|
||||
if (!agent) return 'Unknown device';
|
||||
@@ -76,20 +81,32 @@ export function PasskeySection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
function openRegister() {
|
||||
setRegPassword('');
|
||||
setRegError(null);
|
||||
setShowPasswordModal(true);
|
||||
}
|
||||
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setRegError(null);
|
||||
setRegistering(true);
|
||||
try {
|
||||
await registerPasskey(async () => {
|
||||
const token = await getAccessToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
return token;
|
||||
});
|
||||
}, regPassword);
|
||||
setShowPasswordModal(false);
|
||||
await refetch();
|
||||
toast({ title: 'Passkey registered', variant: 'success' });
|
||||
} catch (err) {
|
||||
// User cancelled the WebAuthn prompt — not an error
|
||||
if (err instanceof Error && err.name === 'NotAllowedError') return;
|
||||
toast({ title: 'Passkey registration failed', description: errorMessage(err), variant: 'error' });
|
||||
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||
setRegistering(false);
|
||||
return;
|
||||
}
|
||||
setRegError(errorMessage(err));
|
||||
} finally {
|
||||
setRegistering(false);
|
||||
}
|
||||
@@ -109,12 +126,13 @@ export function PasskeySection() {
|
||||
const credentials = passkeys ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Passkeys">
|
||||
<p className={styles.description} style={{ marginTop: 0 }}>
|
||||
Use your fingerprint, face, or security key to sign in faster.
|
||||
</p>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Button variant="primary" onClick={handleRegister} loading={registering}>
|
||||
<Button variant="primary" onClick={openRegister}>
|
||||
Register passkey
|
||||
</Button>
|
||||
</div>
|
||||
@@ -160,5 +178,39 @@ export function PasskeySection() {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={showPasswordModal}
|
||||
onClose={() => { if (!registering) setShowPasswordModal(false); }}
|
||||
title="Confirm identity"
|
||||
size="sm"
|
||||
>
|
||||
<p className={styles.description} style={{ marginTop: 0 }}>
|
||||
Enter your password to register a new passkey.
|
||||
</p>
|
||||
{regError && <Alert variant="error">{regError}</Alert>}
|
||||
<form onSubmit={handleRegister} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<FormField label="Password" htmlFor="passkey-reg-password">
|
||||
<Input
|
||||
id="passkey-reg-password"
|
||||
type="password"
|
||||
value={regPassword}
|
||||
onChange={(e) => setRegPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</FormField>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button type="submit" variant="primary" loading={registering} disabled={!regPassword}>
|
||||
Continue
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => setShowPasswordModal(false)} disabled={registering}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user