Skip to content

Passkeys (WebAuthn)

Autentico supports WebAuthn (FIDO2) passkeys via the go-webauthn/webauthn library. Passkeys use public-key cryptography bound to the user’s device — platform authenticators (Face ID, Touch ID, Windows Hello) and hardware security keys (YubiKey, etc.) are both supported.

sequenceDiagram
    actor User
    participant Browser
    participant IdP as Autentico

    Browser->>IdP: GET /oauth2/passkey/login/begin?username=...&client_id=...
    IdP->>IdP: Look up user credentials
    IdP->>Browser: { type: "authentication", challenge_id, options }
    Browser->>Browser: navigator.credentials.get(options)
    User->>Browser: Approve with biometric / security key
    Browser->>IdP: POST /oauth2/passkey/login/finish?challenge_id=X
    IdP->>IdP: Verify assertion — issue auth code
    IdP->>Browser: { redirect: "redirect_uri?code=AUTH_CODE" }
    Browser->>Browser: Navigate to redirect URL

Challenges are short-lived (5 minutes) and single-use. The challenge data is stored in the passkey_challenges table.

New accounts are created through Create account when allow_self_signup is enabled for the client and auth_mode is password_and_passkey or passkey_only. Both the start and completion endpoints check the current policy, including the client’s self-signup override. Disabling signup during an unfinished ceremony prevents its completion. Switching to password also blocks named and discoverable passkey login, including ceremonies already in progress; disabling signup alone still permits existing-user login. Existing users can still add passkeys through Account → Security.

The browser starts a ceremony at GET /oauth2/passkey/register/begin and submits the WebAuthn response to POST /oauth2/passkey/register/finish. Both endpoints use the authentication rate limiter. An HttpOnly, host-only cookie binds the five-minute registration to the initiating browser. Other browsers cannot replace a live pending account or complete its ceremony. The initiating browser can retry; abandoned usernames become available once the registration window expires.

When require_email_verification is enabled, signup requires an email address and configured SMTP delivery. After creating the passkey, the user receives a verification email and sees the verification page. Autentico issues no session or authorization code until the email link is used. Logging in with that passkey, including discoverable login, continues to the verification page until verification succeeds. The page also offers the existing resend action if delivery failed.

The WebAuthn relying party name — shown in the browser’s passkey prompt — is controlled by the passkey_rp_name runtime setting. Set it to your application or organization name.

The relying party ID is derived from AUTENTICO_APP_URL (the hostname component). This is a security boundary in WebAuthn: credentials registered for auth.example.com cannot be used on a different origin.

Set auth_mode to password_and_passkey or passkey_only:

Terminal window
curl -X PUT https://auth.example.com/admin/api/settings \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"auth_mode": "password_and_passkey", "passkey_rp_name": "My Company"}'

The login page will show the passkey option automatically once this is set.

The passkey_login_mode setting controls how passkeys are presented on the login page. Each mode suits different deployment scenarios and user bases.

The user types their username first, then authenticates with their registered passkey. The server looks up the user’s credentials and sends them to the browser as allowCredentials, so the authenticator only prompts for matching passkeys.

This is the safest default — it works with every authenticator type and doesn’t depend on discoverable credential support.

A “Sign in with passkey” button appears on the login page. Clicking it triggers a discoverable credential ceremony — the browser or authenticator shows all passkeys registered for this relying party, and the user picks one. No username is needed.

This works best with:

  • Apple devices — iCloud Keychain shows an account picker natively
  • Windows Hello — built-in account selection
  • Hardware security keys — YubiKeys and other FIDO2 keys with resident key storage

The username and password fields remain visible for users who prefer password login or don’t have a passkey registered.

The browser automatically surfaces registered passkeys in the username field’s autofill dropdown when the login page loads. The user sees their passkey alongside saved passwords without clicking any button — they simply focus the username field and pick a passkey from the autofill suggestions.

This is the most seamless mode for environments where users rely on password managers (Chrome + Google Password Manager, Safari + iCloud Keychain, Firefox). The passkey appears as just another autofill option.

Technically, this uses navigator.credentials.get() with mediation: 'conditional' and adds autocomplete="username webauthn" to the username input. If the browser doesn’t support conditional mediation, the passkey button remains available as a fallback.

No username or password fields are shown — only a “Sign in with passkey” button. This is for deployments that have fully committed to passwordless authentication.

Mode Best for Username required Password fallback
username_first Maximum compatibility, gradual passkey adoption Yes Yes
discoverable Platform authenticators (Apple, Windows Hello), hardware keys No Yes
conditional Password managers, autofill-heavy environments No Yes
passkey_only Fully passwordless deployments No No
Terminal window
curl -X PUT https://auth.example.com/admin/api/settings \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"passkey_login_mode": "discoverable"}'

The setting takes effect immediately — the next login page render will use the new mode.

Discoverable passkey login (discoverable, conditional, and passkey_only modes) relies on resident keys — credentials stored on the authenticator itself, not just on the server. During registration, Autentico requests ResidentKey: preferred, which means most modern authenticators will create a discoverable credential by default.

The authenticator stores a userHandle (the user’s internal ID) alongside the private key. During a discoverable login, the authenticator returns this userHandle in its signed assertion, allowing the server to identify the user without a username.