Passkeys (WebAuthn)
Passkeys are phishing-resistant, hardware-backed credentials that ride on the W3C WebAuthn API. IdentSphere supports them as both a sign-in factor and an MFA-style second factor.
Enrollment
Two-step ceremony:
POST /v1/users/me/passkeys/register/beginreturnsPublicKeyCredentialCreationOptions.- Browser calls
navigator.credentials.create()with those options. POST /v1/users/me/passkeys/register/completewith the result.
@identsphere/react's usePasskeyEnroll hook wraps the three steps.
Sign-in
POST /v1/auth/passkey/login/begin { email }
→ PublicKeyCredentialRequestOptions
browser: navigator.credentials.get(...)
POST /v1/auth/passkey/login/complete { credential }
→ full LoginResponse::Success (cookies set)
The resulting session is AAL2; mfa_verified_at is populated.
Configuration
The Relying Party identity must match your deployment:
| Field | Example |
|---|---|
rp_id | auth.example.com (host only, no scheme/port) |
rp_origin | https://auth.example.com (full URL including scheme) |
rp_name | Acme (shown to user by some authenticators) |
In development, localhost is the only host browsers accept without HTTPS.
Production requires cookies_secure = true and the SDK to be served from
the origin matching rp_origin exactly.
Storage
| What | Where |
|---|---|
| Credential ID | user_passkeys.credential_id (base64url) |
Public key + sign counter (serialized webauthn-rs Passkey) | user_passkeys.public_key (base64url(JSON)) |
| Per-user passkey count cap | 10 |
| In-flight challenge state | passkey_challenges (5-min TTL) |
Limits
- v0.1 requires an email at
login/begin. Discoverable (resident-key) login lands later. - Max 10 enrolled passkeys per user.
- Conditional UI (autofill from the browser) requires the
webauthn-rsconditional-uifeature flag — not enabled in v0.1.
Best practices
- Encourage passkey enrollment in onboarding. They're the strongest factor available to consumer hardware.
- Keep passwords as a fallback for password-managed accounts. Users who lose every passkey AND their authenticator need to be able to recover via password + email.
- Prefer passkey + password over passkey-only until passkey portability across cloud providers stabilizes.
What MFA-vs-Passkey looks like in code
import { useSession } from '@identsphere/react';
function MfaLevel() {
const { data } = useSession();
if (!data) return null;
const method = data.user.auth_method;
// 'password' | 'password_with_mfa' | 'passkey' | 'api_key' | 'social_oauth'
const aalLabel = ({
'password': 'AAL1',
'password_with_mfa': 'AAL2',
'passkey': 'AAL2',
'social_oauth': 'AAL1',
'api_key': 'AAL1',
} as const)[method];
return <span>{aalLabel}</span>;
}