Passkeys & FIDO2
WebAuthn second factor, built on Fido2NetLib
What Orion uses
| Package | Version | Role |
|---|---|---|
Fido2 | 3.0.1 | WebAuthn attestation and assertion verification |
Fido2.AspNet | 3.0.1 | services.AddFido2(...) and the IFido2 service injected into controllers |
Passkeys are a second factor here, not a primary credential: a user signs in with a
password, and the passkey satisfies the outstanding second-factor step. Registering one
automatically sets TwoFactorEnabled on the account.
Configuration
| Variable | Default | Notes |
|---|---|---|
ORION_FIDO2_DOMAIN | host of ORION_BASE_URL |
The WebAuthn Relying Party ID. Registered credentials are bound to it — change it and every existing passkey stops working. |
ORION_FIDO2_ORIGINS | derived from ORION_BASE_URL |
Absolute origins allowed to perform WebAuthn, space or comma separated. Each must be a full URI (https://login.example.com). Invalid entries, or an empty result, throw at startup. |
Registration
From Account → Security, while signed in. Two calls, following the WebAuthn ceremony:
◀── CredentialCreateOptions (challenge, RP, user handle)
Browser ──▶ navigator.credentials.create(…)
Browser ──▶ POST /Management/CompletePasskeyRegistration
◀── attestation verified ▶ Authenticator row, TwoFactorEnabled = true
The credential is stored as an Authenticator with
AuthenticatorType = Passkey. A user may hold several.
Signing in with one
After a password login, the session key carries a mandatory second-factor action and is not yet a valid session. The assertion clears it:
◀── assertion options (challenge, allowCredentials)
Browser ──▶ navigator.credentials.get(…)
POST https://login.shanecraven.com/federation/twofactor/passkey/complete?actionId=N
◀── verified ▶ pending action deleted ▶ session usable
Both the OAuth second-factor page and the legacy federation page use these same endpoints. Completing 2FA is recorded by deleting the pending action row — until that happens the session resolver rejects the key, so a password alone is never enough.
Implementation notes
- Fido2NetLib types are System.Text.Json. The MVC pipeline here uses Newtonsoft, so
passkey completion endpoints read the raw body and deserialise with
System.Text.Jsonmanually. Using[FromBody]silently produces an empty object and the ceremony fails with no useful error. - Cookies are HttpOnly. A page cannot read the session key from
document.cookieto post it back; the server resolves the caller itself. An earlier version of the device page tried exactly that and could never work. - TOTP is the alternative factor. Users with an authenticator app satisfy the same pending action via a code instead — see the OAuth second-factor page.
If a passkey will not work
- Nothing happens on click — the browser requires a secure context. Use HTTPS
(
localhostis exempt). - "Not allowed" / origin errors — the page origin is not in
ORION_FIDO2_ORIGINS, or does not matchORION_FIDO2_DOMAIN. - Worked before, now fails for everyone — the RP ID changed. Existing credentials are bound to the old one and cannot be recovered; users must re-enrol.
- Server will not start, complaining about origins — an entry in
ORION_FIDO2_ORIGINSis not an absolute URI, or the list resolved to empty. - 2FA is on but no authenticator is active — that account cannot complete sign-in and there is no self-service recovery. An administrator must clear the flag directly.