DEV Community

Cover image for Building One Tap for PingFederate, Part 1: Architecture and the Secure Account Bridge
DarkEdges
DarkEdges

Posted on

Building One Tap for PingFederate, Part 1: Architecture and the Secure Account Bridge

Modern sign-in experiences often remember accounts and let a returning user
continue with one click. Reproducing that pattern with PingFederate requires
more than drawing an account picker. The browser, PingFederate, and the web
application each own different state, and treating those states as one session
causes repeated password prompts, stale panels, and broken logout behavior.

This series explains a working local implementation for PingFederate 12.3.3.
The complete source is available at
github.com/darkedges/pingfedonetap.

The behavior we want

The target experience has four rules:

  1. A new visitor with no remembered identifier should not see a popup.
  2. The first sign-in should use the normal PingFederate authentication policy.
  3. A valid PingFederate session should let a remembered account continue without another password prompt.
  4. Logging out of the demo application should clear only the application session, then allow the account picker to appear again.

The important distinction is that a remembered account is not automatically a
passwordless account. The account chooser answers "who might sign in?" A valid
PingFederate authentication session answers "has this user already proved who
they are?" The application session answers "is this browser tab currently
signed in to this application?"

The three state owners

Web application
  Owns the local signed-in state and OIDC callback result

PingFederate authentication session
  Determines whether another password challenge is necessary

Identifier First adapter cookie
  Remembers identifiers that completed authentication successfully
Enter fullscreen mode Exit fullscreen mode

These states must be coordinated, not merged.

When the application receives tokens, it suppresses the chooser and hides its
sign-in button. When the user selects "Log out of demo," the application clears
its own state and resumes the chooser. It deliberately does not terminate the
PingFederate session. That is why selecting a remembered account can complete
silently after application logout.

Why the browser cannot simply read the account cookie

PingFederate's Identifier First adapter owns a persistent HttpOnly cookie named
identifierFirstAdapter.previous.subjects. HttpOnly is correct because page
JavaScript should not read authentication cookies.

The demo runs at http://localhost:8080, while PingFederate runs at
https://localhost:9031. The application is also on a different origin, so it
cannot inspect PingFederate cookies directly.

The solution introduces a small status-only PingFederate adapter. It runs on
the PingFederate origin, reads the native Identifier First cookie, and returns a
sanitized list through an origin-restricted iframe bridge. The browser widget
renders the chooser only when that list contains accounts.

Demo application
  |
  | hidden sandboxed iframe with nonce
  v
PingFederate /ext/one-tap/status
  |
  | reads HttpOnly Identifier First cookie
  v
Allowed parent origin receives sanitized users via postMessage
Enter fullscreen mode Exit fullscreen mode

Authentication flow

The first login uses an OIDC Authorization Code flow with PKCE:

  1. The application sends the browser to PingFederate.
  2. Identifier First collects or selects the identifier.
  3. The HTML Form adapter validates the password.
  4. PingFederate records the successful identifier and establishes its session.
  5. The callback validates state and exchanges the code with PKCE.
  6. The application marks itself signed in and suppresses the account chooser.

For a remembered account, the widget first attempts authorization with
prompt=none. If PingFederate can reuse its session, tokens return without a
password prompt. If silent authentication cannot complete, the widget falls
back to an interactive top-level request without forcing prompt=login.

Build the status bridge

The custom OneTapStatusAdapter registers this extension endpoint:

/ext/one-tap/status
Enter fullscreen mode Exit fullscreen mode

The widget loads the endpoint in a hidden iframe. The adapter reads the cookie
server-side and renders a small Velocity template. That template sends a
sanitized account array to the parent with window.parent.postMessage().

The endpoint is status-only. It is not inserted into the interactive
authentication policy and remains separate from the Identifier First and HTML
Form sequence.

Decode the native cookie

The Identifier First cookie contains a URL-encoded, comma-separated list of
Base64-encoded identifiers. The adapter decodes each value and returns display
objects. It never returns passwords, tokens, or the raw cookie.

Supporting PingFederate's native representation was important. Inventing a
second account cookie would have created another source of identity state and
made lifecycle behavior harder to reason about.

Secure the cross-origin response

A cross-origin account bridge needs several controls working together.

Exact origin allowlist

Configured origins are normalized to a scheme, host, and optional non-default
port. User information, paths, query strings, and fragments are rejected.

The same allowlist controls CORS, CSP, and the permitted postMessage target.

Per-request nonce

The widget generates a random nonce and includes it as pfOtNonce. The adapter
accepts only 32 to 128 hexadecimal characters and echoes the value in the
bridge message.

The parent accepts the response only when the origin, source window, message
type, and nonce all match the iframe request it created.

Sandboxed iframe

The widget creates an invisible iframe with sandbox="allow-scripts". The
bridge can execute its small script, but it cannot submit forms, navigate the
top-level page, or gain broader browser capabilities.

Explicit postMessage target

The bridge never posts to "*". It checks document.referrer against the
allowlist and posts only to an accepted origin. The parent independently
validates the sender origin and nonce.

Narrow framing policy

PingFederate normally emits X-Frame-Options: SAMEORIGIN. That blocks the
intentional cross-origin iframe. The dedicated endpoint removes that header
and replaces it with a narrow policy:

Content-Security-Policy: frame-ancestors http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

It also returns Cache-Control: no-store so account status is not cached.

Avoid the first-request 404

An extension handler registered only when an adapter instance is first used
can return 404 when the widget calls it before any authentication flow has
initialized the adapter.

The implementation registers the handler in the adapter constructor and again
during configuration. The container also provides
ONE_TAP_ALLOWED_ORIGINS during plugin discovery so early registration has a
safe origin policy.

Give the host explicit controls

The browser widget exposes a deliberately small API:

PfOneTap.dismiss();
PfOneTap.suppress();
PfOneTap.resume();
PfOneTap.refresh();
PfOneTap.signIn(username, displayName);
Enter fullscreen mode Exit fullscreen mode

The host calls suppress() after establishing its application session. On
application logout, it calls resume() and refresh() to request fresh status
with a new nonce.

Design and security lessons

  • Account discovery and authentication are separate concerns.
  • Application logout and identity provider logout should be separate actions.
  • A remembered identifier must never be treated as proof of authentication.
  • Host applications need explicit suppress, resume, and refresh controls for embedded sign-in widgets.
  • Authorization Code with PKCE is the correct baseline for a browser public client.
  • Keep the origin allowlist exact and environment-specific.
  • Never return credentials, tokens, or raw cookie data.
  • Validate both event.origin and event.source in the parent.
  • Bind every bridge response to a fresh nonce.
  • Apply frame-ancestors only to the dedicated bridge endpoint.

In part 2, we will package the adapter with Docker and provision the complete
authentication and OIDC configuration with Terraform.

Repository: https://github.com/darkedges/pingfedonetap

Video demo: https://youtu.be/KuVNjWiZrAk

PingIdentity #PingFederate #OIDC #Authentication #Identity

Top comments (0)