Passkey Authenticator
passkey authenticates a user with a WebAuthn/FIDO2 credential — a
device's platform authenticator (Face ID, Windows Hello, a fingerprint
sensor) or a physical security key. O2ID stores only the credential's
public key and signature counter, encrypted per tenant like every other
authenticator secret; the private key never leaves the user's device.
Unlike TOTP or the OTP-style authenticators, a passkey ceremony needs a
browser: enrollment calls navigator.credentials.create() and sign-in
calls navigator.credentials.get(). There's no CLI enrollment command —
enrollment is a two-step REST exchange your own client (a web app using an
O2ID SDK, for example) drives directly.
Configure a passkey authenticator in a tenant
WebAuthn's relying party ID is domain-only and can't express O2ID's
/t/{tenant} path-based tenancy, so every tenant on a deployment shares
one relying party — set rpId/rpOrigin once to match your deployment's
own domain, not per tenant the way TOTP's issuer is.
o2idctl authenticators create --type passkey --display-name "Passkey" \
--field rpId=auth.example.com \
--field rpOrigin=https://auth.example.com
| Field | Default | Description |
|---|---|---|
rpId | — (required) | Relying party ID: the domain your deployment is served from, no scheme or port |
rpOrigin | — (required) | Relying party origin: the full https:// URL your deployment is served from |
rpDisplayName | O2ID | Name shown to the user during registration |
Enroll a passkey
Enrollment is a begin/finish pair around the browser's
navigator.credentials.create() call. begin returns
CredentialCreationOptions JSON to pass straight to
navigator.credentials.create({ publicKey: ... }) (after decoding its
base64url fields to ArrayBuffers) and a sessionId to echo back:
POST /users/{id}/authenticator-enrollments/webauthn/begin
{ "authenticatorId": "<authenticator-id>" }
-> { "sessionId": "...", "options": { "publicKey": { ... } } }
POST /users/{id}/authenticator-enrollments/webauthn/finish
{ "sessionId": "...", "response": { ... browser's PublicKeyCredential, JSON-encoded ... } }
-> { "id": "...", "userId": "...", "authenticatorId": "...", "verifiedAt": "..." }
A completed registration ceremony is itself the verification — there's no separate code-entry step like TOTP's.
Use a passkey in a flow
Reference the configured authenticator ID from an authenticator node,
same as any other factor:
{
"passkey": {
"type": "authenticator",
"onSuccess": "allow",
"onFailure": "passkey",
"properties": {
"authenticatorId": "<authenticator-id>",
"maxAttempts": 5,
"onLockout": "deny"
}
}
}
When a run lands on this node, O2ID mints a fresh assertion challenge and
the login portal drives navigator.credentials.get() automatically —
there's no field for the user to fill in first. See
Flows for a complete login flow example.