Skip to content

Passkeys

People sign in to Omxus with Face ID, Touch ID or Windows Hello. Nothing typed. Omxus has two kinds of passkey, and every passkey belongs to one hostname. Know both facts and passkey sign-in never surprises you.

Two kinds

Kind Made where What the server keeps How it signs in
Account passkey "Save a passkey" on the standard door (/accounts/passkey/register/*) One row: credential id, public key, RP id A WebAuthn assertion, checked against the stored public key
Identity passkey The full Omxus door (omxus-credential.mjs) Nothing. The identity is the passkey's PRF output (salt omxus-seed:v1) The browser derives a per-site key from that seed and signs a server nonce (/accounts/omxus/challenge, then /accounts/omxus/finish)

An account passkey is a login method on an account. An identity passkey is the identity: the same passkey gives the same address every time, and a different site gets a different address, so sites cannot compare notes.

"Use a passkey" on the standard door asks for both at once. The request carries the PRF extension, so an identity passkey hands over its seed and an account passkey is matched by its row (passkeyLogin in omxus-accounts.mjs).

If you use OIDC, you do nothing

Sites that sign people in through Any app, via OIDC send people to auth.omxus.com. Passkeys happen there. Your site receives the same ID token either way.

One hostname each

WebAuthn ties a passkey to its RP id. Omxus uses the hostname of the page that asked:

  • On the hosted door, that is auth.omxus.com.
  • If you build your own sign-in page on the accounts API, the RP id is your page's hostname. A passkey saved on app.example.com is never offered on www.example.com.

The server refuses a passkey whose stored RP id differs from the calling page's host (401 passkey_unknown).

The accounts API calls

All are POST. Challenges last 300 seconds; user verification is required.

Call Body Answer
/accounts/passkey/register/options (signed in) WebAuthn creation options
/accounts/passkey/register/finish {credential} Saved, or 400 passkey_failed
/accounts/passkey/login/options none {challenge, rpId, userVerification, timeout}
/accounts/passkey/login/finish {credential} A session, or 401 passkey_failed / passkey_unknown
/accounts/omxus/challenge {address} {nonce, issuedAt, ttlSeconds}
/accounts/omxus/finish {signed, root, create?} A session, or 409 omxus_unknown with a ticket
/accounts/omxus/create {ticket} A new account holding that key
/accounts/omxus/link {ticket} (signed in) That key added to the signed-in account

A key no account knows yet

When an identity passkey's key matches no account, /accounts/omxus/finish answers 409 omxus_unknown with a ticket that lasts ten minutes. Show one step with two choices:

  1. Create a new account: POST /accounts/omxus/create with the ticket.
  2. Sign in to link it: sign in with password or code, then POST /accounts/omxus/link with the ticket.

Send create: true to /accounts/omxus/finish to skip the question and create the account at once. The SDK does both: hasPendingKey() tells you a ticket is waiting.

A passkey the server never saved

If a save never finished, the device holds a passkey the server has no row for. The server refuses it, and the SDK tells the device to drop it with PublicKeyCredential.signalUnknownCredential where the browser supports it. The person stops seeing a passkey that cannot work.

Limits today

  • One identity address per account. A second device's identity passkey has a different seed, so it needs its own link step.
  • An account passkey works only on the host it was saved on.

Checked

Routes, TTLs and error codes checked against server/accounts/accounts-routes.js on 30 Oct 2026. The passkey suite (node sdk/passkey.test.mjs in the auth repo) passes 58 of 58. The tables above are not a runnable sample; the SDK calls them for you.