feat: Passkey (WebAuthn) login (#1111)

* feat(auth): passkey (WebAuthn) login — server endpoints, schema + admin toggle

Add @simplewebauthn/server registration and primary (discoverable) login ceremonies under /api/auth/passkey, a webauthn_credentials + single-use webauthn_challenges schema (migration), the instance-wide passkey_login toggle (default off) enforced before auth by a guard, and require_mfa satisfaction via a verified passkey. RP ID/origin come only from server config (webauthn_rp_id/origins -> APP_URL), never request headers.

* feat(auth): passkey enrolment, login button + admin settings UI

PasskeysSection in account settings (add/rename/remove with a current-password step-up), a 'Sign in with a passkey' button on the login page, the admin enable + RP-ID/origins controls, and a per-user admin reset action.

* i18n(auth): passkey strings across all locales

Add login/settings/admin passkey keys to en and all 19 translated locales.
This commit is contained in:
Maurice
2026-06-05 18:54:13 +02:00
committed by GitHub
parent 247433fb2a
commit a876fb2634
83 changed files with 2421 additions and 8 deletions
+20 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import { adminApi } from '../../api/client'
import Modal from '../../components/shared/Modal'
import CustomSelect from '../../components/shared/CustomSelect'
import { CheckCircle, ArrowUpCircle, ExternalLink, RefreshCw, AlertTriangle } from 'lucide-react'
import { CheckCircle, ArrowUpCircle, ExternalLink, RefreshCw, AlertTriangle, Fingerprint } from 'lucide-react'
import type { TranslationFn } from '../../types'
import type { useAdmin } from './useAdmin'
@@ -157,6 +157,25 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea
]}
/>
</div>
<div className="pt-3 border-t border-slate-100">
<p className="text-xs text-slate-400 mb-2">{t('admin.passkey.resetHint')}</p>
<button
type="button"
onClick={async () => {
if (!editingUser) return
if (!confirm(t('admin.passkey.resetConfirm', { name: editingUser.username }))) return
try {
const r = await adminApi.resetUserPasskeys(editingUser.id)
toast.success(t('admin.passkey.resetDone', { count: r.deleted ?? 0 }))
} catch {
toast.error(t('common.error'))
}
}}
className="flex items-center gap-2 px-3 py-2 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50"
>
<Fingerprint size={14} /> {t('admin.passkey.reset')}
</button>
</div>
</div>
)}
</Modal>