The pi.flow Protocol¶
pi.flow is PingOne's own interactive sign-on protocol, emulated here as a real, working state
machine (packages/api/src/lib/authFlow/emulatedProvider.ts) rather than a static mock. This page
describes the shape of the protocol; see Architecture
for how it fits alongside the alternative PingFederate-backed provider.
The basic exchange¶
GET /:environmentId/as/authorizewithresponse_mode=pi.flow(this emulator only implements that one response mode) starts a flow and returns{ id, status }as a plain 200 JSON response — no browser redirect happens at this step.POST /:environmentId/flows/:flowIdsubmits whatever the currentstatusis waiting on. The request body's shape depends entirely onstatus— the caller is expected to know what a given status requires (see below).GET /:environmentId/flows/:flowIdre-fetches the flow's current state without submitting anything — used to pick up fresh embedded data (e.g. WebAuthn ceremony options) without advancing the flow.- Once the flow reaches
status: "COMPLETED", the response carriesauthorizeResponse— an authorizationcode(or, for an implicit-style request,access_token/id_tokendirectly) plus the originalstate.
Statuses¶
| Status | Meaning |
|---|---|
USERNAME_PASSWORD_REQUIRED |
Submit {username, password} — or {credential} (a WebAuthn AuthenticationResponseJSON) for usernameless passkey sign-in on this same status. |
IDENTIFIER_REQUIRED |
An identifier-first flow's initial step — submit {username} alone before password is even asked for. |
VERIFICATION_CODE_REQUIRED |
Email/SMS verification code step. |
MFA_REQUIRED / MFA_OTP_REQUIRED |
MFA device selection / OTP entry. |
PASSKEY_REGISTRATION_REQUIRED |
The client should call navigator.credentials.create() using the embedded WebAuthn options, then submit {credential} — or {skip: true} to decline enrollment. |
PASSKEY_REQUIRED |
The client should call navigator.credentials.get() using the embedded WebAuthn options, then submit {credential}. |
PASSWORD_RESET_REQUIRED |
Forced password reset before the flow can continue. |
PROFILE_UPDATE_REQUIRED |
Progressive profiling — submit whatever profile fields were requested. |
AGREEMENT_REQUIRED |
Submit {accept: true} for a terms-of-service/agreement gate. |
IDENTITY_PROVIDER_REQUIRED |
The flow embeds redirect target(s) for federated sign-in instead of continuing locally (_embedded.identityProviders). |
DEVICE_USER_CODE_REQUIRED / DEVICE_CONSENT_REQUIRED |
RFC 8628 device authorization grant's browser-side verification steps. |
COMPLETED |
Terminal — authorizeResponse is populated. |
_embedded¶
Depending on status, a flow response can carry one of:
{ user: { id } }— present once a user is associated with the flow (login succeeded, or the moment a self-registration creates the new account).{ identityProviders: [...] }— forIDENTITY_PROVIDER_REQUIRED.{ webauthn: ... }— the WebAuthn ceremony options forPASSKEY_REGISTRATION_REQUIRED/PASSKEY_REQUIRED(and, if passkey-after-login enrollment is enabled for the environment, embedded speculatively onUSERNAME_PASSWORD_REQUIREDtoo, to support autofill).
WebAuthn specifics¶
Both passkey statuses need the calling page's Origin header — rpID and the expected origin are
resolved per-request (lib/webauthn.ts's resolveRp), not hardcoded, since one emulator instance
backs many real subdomains. This applies to both POST /flows/:id (submitting a ceremony response)
and GET /flows/:id (re-fetching fresh, unused challenge options, e.g. after a page reload).
Beyond plain pi.flow¶
A few related flow-shaped protocols reuse the same /flows/:id machinery under the hood
(AuthorizationFlowProvider in lib/authFlow/types.ts):
- DaVinci-hosted flows — if the target application has a DaVinci flow policy assigned, real
PingOne (and this emulator) returns a different envelope instead of the plain
{id, status}shape:interactionId,interactionToken,connectionId,capabilityName. Subsequent steps go throughPOST /davinci/connections/:connectionId/capabilities/:capabilityNameinstead ofPOST /flows/:id. - Device authorization grant (RFC 8628) —
POST /as/device_authorizationstarts a device's polling record; the user's own browser-side verification (GET /device) reuses/flows/:idwith no client/redirect context until the user submits theiruser_code. - SAML —
POST /saml20/idp/ssostarts a browser-side flow resolved by SP entity id instead of an OAuthclient_id, also reusing/flows/:id;GET /saml20/resumebuilds the final SAML Response assertion.