Skip to main content
Version: Next

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
FieldDefaultDescription
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
rpDisplayNameO2IDName 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.