Passkeys & FIDO2

WebAuthn second factor, built on Fido2NetLib

Back to Documentation

What Orion uses

PackageVersionRole
Fido23.0.1WebAuthn attestation and assertion verification
Fido2.AspNet3.0.1services.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

VariableDefaultNotes
ORION_FIDO2_DOMAINhost 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_ORIGINSderived 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.
The RP ID is effectively permanent. Credentials are scoped to it by the browser, so moving the server to a different domain invalidates every registered passkey and every user must re-enrol. Decide the hostname before enrolling anyone.

Registration

From Account → Security, while signed in. Two calls, following the WebAuthn ceremony:

Browser ──▶ POST /Management/BeginPasskeyRegistration?name=…
    ◀── 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:

POST https://login.shanecraven.com/federation/twofactor/passkey/begin  { actionId }
    ◀── 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.

Ownership is enforced. An assertion may only clear a pending action belonging to the user it authenticated. Otherwise someone holding a victim's password could satisfy the victim's second factor with their own passkey.

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.Json manually. 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.cookie to 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 (localhost is exempt).
  • "Not allowed" / origin errors — the page origin is not in ORION_FIDO2_ORIGINS, or does not match ORION_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_ORIGINS is 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.
Authentication Configuration All Documentation