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 callsnavigator.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 callsnavigator.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).