Skip to content

Passkeys

Passkey support (packages/api/src/lib/webauthn.ts) uses real WebAuthn ceremonies via @simplewebauthn/server — not a mock — wired into the pi.flow protocol as two statuses:

  • PASSKEY_REGISTRATION_REQUIRED — offered after a successful password sign-in, when the environment has passkey enrollment enabled and the user has no passkey registered yet. The client calls navigator.credentials.create() with the embedded options, then submits {credential} — or {skip: true} to decline.
  • PASSKEY_REQUIRED — a full passkey sign-in (or step-up) challenge. The client calls navigator.credentials.get() and submits {credential}.

Usernameless sign-in works too: submitting {credential} directly on USERNAME_PASSWORD_REQUIRED (without ever collecting a username) is accepted as a discoverable-credential passkey login on the same status as a normal password submission.

Per-origin rpID resolution

WebAuthn's Relying Party ID and expected origin can't be a fixed value here: one emulator instance backs many real subdomains (partner portals, demo companies, etc.), so both are resolved per-request from the caller's Origin header (resolveRp(origin)), not hardcoded. Every call into beginPasskeyRegistration / completePasskeyRegistration / beginPasskeyAuthentication / completePasskeyAuthentication takes the request's origin explicitly for this reason.

Discoverable credentials

Registration uses residentKey: 'preferred', so credentials are discoverable/usernameless by default — this is what makes the identifier-autofill flow below possible.

Autofill (conditional UI)

A login form can offer passkey autofill without any extra user action, via startAuthentication({ useBrowserAutofill: true }) paired with autoComplete="username webauthn" on the identifier <input>. To support this, GET /:environmentId/flows/:flowId embeds _embedded.webauthn speculatively even on USERNAME_PASSWORD_REQUIRED (not just the two dedicated passkey statuses), whenever the environment has this feature enabled.

Feature gating

Passkey behavior is opt-in per environment, via the self_service_settings resource's manageAuthentication config (packages/api/src/routes/v1/environmentSettings.ts):

Field Effect
manageMfaDevicesViaMyAccount Lets a signed-in user manage their own MFA devices.
enrollPasskeysAfterLogin Enables the PASSKEY_REGISTRATION_REQUIRED nudge after a successful password login, and speculative _embedded.webauthn on USERNAME_PASSWORD_REQUIRED for autofill.

Both default to off — matching this codebase's convention of not unconditionally changing behavior for every environment a shared emulator serves. In Terraform, environments without a real pingone_* field for this yet enable it via a null_resource + PowerShell local-exec hitting the raw REST endpoint directly (see terraform/self-service-darkedges-admin-console/07_passkey_enrollment.tf for the pattern).