Skip to content

Passkey login

Org-member passkey endpoints under /api/passkey. WebAuthn via the FIDO2 service; on register/login a web origin gets an HttpOnly cookie (SDK callers keep the body token).

One credential per account (stable user handle)

Section titled “One credential per account (stable user handle)”

Every enrollment endpoint sets the WebAuthn user.id (userHandle) to a deterministic value derived from the normalized email (DeviceHelper.StableUserHandle), not a random or account-row id. Platform authenticators (Windows Hello, Touch ID) de-duplicate resident credentials by (rpId, userHandle), so re-registering the same email — including after a database reset — replaces the existing passkey instead of piling up a new one. This keeps the authenticator’s credential set at one per account, which is what keeps the sign-in dialog fast (it has a single credential to enumerate, not dozens).

The userHandle is not used for identity: login verification always resolves the account from the returned credentialId against the database, never from the handle.

Method · RoutePurposeSource
POST /register/optionsAttestation options (Username, optional ApplicationKey, SecurityMode)PasskeyEndpoints.cs:91
POST /registerComplete registration → auto-login (cookie issued):110
Method · RoutePurposeSource
POST /add/optionsEnrollment options (authenticated). Passes ExcludeCredentials to prevent the cloud-sync duplicate-accumulation issue:143
POST /addAdd a passkey to the current user (optional orgSlug):178
Method · RoutePurposeSource
POST /login/optionsAssertion options (optional email + ApplicationKey):215
POST /loginComplete assertion → cookie / token:226

GET /exists?email= (does the user exist / has passkey / login provider) and GET /browser-match?fingerprint= (authenticated) round out the set.