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.
Passkey authentication flow
Section titled “Passkey authentication flow”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.
Passkey-only first login (registration flow)
Section titled “Passkey-only first login (registration flow)”In passkey_only mode, users who have no registered passkeys are walked through registration on their first login:
sequenceDiagram
actor User
participant Browser
participant IdP as Autentico
Browser->>IdP: GET /oauth2/passkey/login/begin?username=...
IdP->>IdP: No credentials found + passkey_only mode
IdP->>Browser: { type: "registration", challenge_id, options }
Browser->>Browser: navigator.credentials.create(options)
User->>Browser: Create passkey with biometric / key
Browser->>IdP: POST /oauth2/passkey/register/finish?challenge_id=X
IdP->>IdP: Store credential — issue auth code
IdP->>Browser: { redirect: "redirect_uri?code=AUTH_CODE" }
After first login, the registered credential is stored in passkey_credentials and used for all subsequent logins.
Relying party configuration
Section titled “Relying party configuration”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.
Enabling passkeys
Section titled “Enabling passkeys”Set auth_mode to password_and_passkey or passkey_only:
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.
Login modes
Section titled “Login modes”The passkey_login_mode setting controls how passkeys are presented on the login page. Each mode suits different deployment scenarios and user bases.
username_first (default)
Section titled “username_first (default)”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.
discoverable
Section titled “discoverable”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.
conditional
Section titled “conditional”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.
passkey_only
Section titled “passkey_only”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.
Choosing a mode
Section titled “Choosing a mode”| 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 |
Configuring via API
Section titled “Configuring via API”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 credentials (resident keys)
Section titled “Discoverable credentials (resident keys)”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.