feat: always enable WebAuthn in MFA factors and add passkey registration
- Sync vendor auth policy to Logto sign-in experience on save and on startup. Always include WebAuthn + TOTP + BackupCode in MFA factors when MFA is enabled — no reason to gate passkeys behind a toggle. - Enable Logto Account Center on startup for user-facing MFA management. - Add passkey registration to account settings via Logto Account API. Frontend calls Logto directly (same domain) for the WebAuthn ceremony: generate options, browser credential creation, verify, and bind. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
useAccountRenamePasskey,
|
||||
useAccountDeletePasskey,
|
||||
} from '../../api/account-hooks';
|
||||
import { registerPasskey } from '../../api/logto-account-api';
|
||||
import styles from '../../styles/platform.module.css';
|
||||
|
||||
export function PasskeyNudgeBanner() {
|
||||
@@ -41,12 +43,14 @@ export function PasskeyNudgeBanner() {
|
||||
|
||||
export function PasskeySection() {
|
||||
const { toast } = useToast();
|
||||
const { data: passkeys, isLoading } = useAccountPasskeyList();
|
||||
const { getAccessToken } = useLogto();
|
||||
const { data: passkeys, isLoading, refetch } = useAccountPasskeyList();
|
||||
const renamePasskey = useAccountRenamePasskey();
|
||||
const deletePasskey = useAccountDeletePasskey();
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||
const [registering, setRegistering] = useState(false);
|
||||
|
||||
function parseAgent(agent: string | null): string {
|
||||
if (!agent) return 'Unknown device';
|
||||
@@ -72,6 +76,25 @@ export function PasskeySection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
setRegistering(true);
|
||||
try {
|
||||
await registerPasskey(async () => {
|
||||
const token = await getAccessToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
return token;
|
||||
});
|
||||
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' });
|
||||
} finally {
|
||||
setRegistering(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
try {
|
||||
await deletePasskey.mutateAsync(id);
|
||||
@@ -90,9 +113,14 @@ export function PasskeySection() {
|
||||
<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}>
|
||||
Register passkey
|
||||
</Button>
|
||||
</div>
|
||||
{credentials.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}>
|
||||
No passkeys registered. Passkeys can be registered during sign-in when prompted.
|
||||
No passkeys registered yet.
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
|
||||
Reference in New Issue
Block a user