Prerequisites
Before you start, make sure you have:- A Prelude account with access to Prelude Auth
- An Application ID (
appID) — see Applications - Your Management API key for backend calls
- A frontend served over HTTPS, or
http://localhost:<port>for local development — the WebAuthn API refuses any other origin
Set up passkey authentication
Configure the WebAuthn Relying Party identity for your app. The RP identity is shared across every passkey ceremony — changing it after credentials are registered invalidates them at the authenticator layer, so set it once per environment.Create a passkey configuration
| Field | Required | Description |
|---|---|---|
rp_id | Yes | The Relying Party identifier — the effective domain (no scheme, no port). Credentials are scoped to this RPID; changing it later breaks existing credentials. |
rp_name | Yes | Human-readable display name shown by authenticators during ceremonies. |
allowed_origins | Yes | List of permitted origins (scheme + host + optional port). Must be a superset of the RPID. For local development, http://localhost:<port> is accepted. |
user_verification | No | required (default), preferred, or discouraged. Use required for MFA so the ceremony proves something the user knows or is, not just possession. |
attestation_preference | No | none (default), indirect, direct, or enterprise. none is privacy-preserving and sufficient for most deployments. Required direct or enterprise when you want to enforce the AAGUID allowlist below. |
login_enabled | No | Defaults to false. Set to true to opt the app into primary-factor passwordless passkey login. See Enable passwordless login below. |
granted_scopes | No | Session scopes attached to the session minted by a primary-factor passkey login, mirroring the per-OAuth-provider and per-OTP granted scopes. Only applies to the passwordless login flow, not step-up. Empty grants no extra scopes. |
aaguid_allowlist | No | List of authenticator-model AAGUIDs (UUID strings) to accept. Empty list disables the filter. See Enterprise authenticator policy below. |
aaguid_blocklist | No | List of AAGUIDs to reject outright. Same shape as the allowlist. |
Declare verify_passkey on the step-up configuration
Add the step key to your step-up configuration and reference it from any scope whose challenge should require a passkey.A registered passkey shows up on the user as an identifier of type
passkey, so direct-mode entries select on it via identifier_types like any other identifier.To express a passkey-or-OTP fallback, list two direct entries on the same scope — the passkey-gated one first, the OTP fallback second. The runtime serves the first one whose identifier types the user holds:Enable passwordless login (optional)
Setlogin_enabled: true on the PasskeyConfig to opt the app into primary-factor passkey sign-in. While the flag is on, registration also requests a discoverable credential (residentKey: required) so the resulting passkey shows up in the browser’s autofill chip.
passkey_registration_failed after the flag flips on.
Enterprise authenticator policy (optional)
Restrict registration to specific authenticator models via the AAGUID allowlist / blocklist on the PasskeyConfig. Pairs withattestation_preference: "direct" or "enterprise" — with "none" most authenticators return an all-zeros AAGUID and the allowlist matches nothing.
Subscribe to passkey lifecycle events (optional)
Three webhook events surface passkey activity for audit and user notifications:| Event | When |
|---|---|
user.passkey.registered | A user completes the registration ceremony. The “we added a new passkey to your account — wasn’t you?” notification flow keys on this event. |
user.passkey.deleted | A user removes a credential (via the SDK or via the compromise-response /me/revoke?target=all flow). |
user.passkey.assertion_failed | An assertion fails to verify — bad signature, sign-count regression, expired login token, etc. Useful for brute-force / cloned-authenticator monitoring. |
Surface passkey state in access tokens (optional)
The custom-claims pipeline exposes ahas_passkey template input. Map it on your app’s claims configuration to let your frontend decide whether to prompt the user to enrol: