<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Ping Identity Developer Portal Blog</title><description>News, guidance, and deep dives from the teams building Ping Identity developer tools.</description><link>https://developer.pingidentity.com/</link><item><title>How expensive is a token exchange, really?</title><link>https://developer.pingidentity.com/blog/how-expensive-is-a-token-exchange-really/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/how-expensive-is-a-token-exchange-really/</guid><description>A load-test of RFC 8693 token exchange against a two-engine PingFederate cluster on Kubernetes.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/pingfederate-token-exchange-performance-test/&quot;&gt;Federico Carbone’s blog site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;Token exchange is gaining popularity as a way to cross trust boundaries in agentic architectures, and I have described the pattern in earlier posts on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/spiffe-token-exchange-for-agent-workloads/&quot;&gt;SPIFFE workload identity&lt;/a&gt; and on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/centralizing-mcp-authorization-with-p1authorize-part-1-azure-apim/&quot;&gt;centralizing MCP authorization with PingAuthorize&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Whenever I propose it, the same performance concern comes up: every extra exchange adds a hop to the authorization server, and a chatty agent could multiply that hop many times. The concern is reasonable, so I measured the token exchange performance against a two-engine PingFederate cluster on Kubernetes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;: At 500 exchanges per second the p95 latency was 9.5 milliseconds, with zero failures across the whole campaign. CPU consumption scaled approximately linearly with the exchange rate across the tested range, corresponding to about 5 CPU-ms of aggregate PingFederate engine CPU per completed exchange.&lt;/p&gt;
&lt;p&gt;The test stops at 500 exchanges per second by choice. The goal was to establish a representative baseline rather than determine the cluster’s saturation point. The tests were also run in a shared environment without dedicated infrastructure.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;The deployment is a standard Ping Identity Kubernetes topology: one administration Pod and two PingFederate engine Pods behind a single Kubernetes Service. The test assumes an agentic workload where the agents and PingFederate run within the same Kubernetes environment. The measured path therefore reflects the expected production service-to-service path and intentionally excludes external ingress and user-to-cluster network latency.&lt;/p&gt;
&lt;p&gt;Accordingly, load is generated by ten k6 agent Pods running inside the same cluster. Everything runs in an Amazon EKS cluster in eu-west-1: six worker nodes, all r5.xlarge instances (4 vCPU and 32 GiB each), running Amazon Linux 2023.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; TB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    subgraph&lt;/span&gt;&lt;span&gt; CL&lt;/span&gt;&lt;span&gt;[&quot;&lt;/span&gt;&lt;span&gt;Amazon EKS cluster &lt;/span&gt;&lt;span&gt;— &lt;/span&gt;&lt;span&gt;eu-west-1 &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;6 nodes, r5.xlarge&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        direction TB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        subgraph&lt;/span&gt;&lt;span&gt; LB&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Load generation&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            K1&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;k6 agent-0&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            K2&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;k6 agent-1&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            KM&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;…&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            K9&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;k6 agent-9&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            JB&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Kubernetes Job&amp;lt;br/&amp;gt;10 indexed completions&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        subgraph&lt;/span&gt;&lt;span&gt; PF&lt;/span&gt;&lt;span&gt;[&quot;&lt;/span&gt;&lt;span&gt;PingFederate &lt;/span&gt;&lt;span&gt;— &lt;/span&gt;&lt;span&gt;namespace pf-perf&lt;/span&gt;&lt;span&gt;&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            direction TB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            ADM&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;pf-admin&amp;lt;br/&amp;gt;1 Pod, 2 CPU / 4 GiB limit&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            ENG1&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;pf-engine-0&amp;lt;br/&amp;gt;2 CPU req / 4 CPU lim&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            ENG2&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;pf-engine-1&amp;lt;br/&amp;gt;2 CPU req / 4 CPU lim&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            SVC&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Kubernetes Service&amp;lt;br/&amp;gt;port 9031&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;perf-jwks-server Pod&amp;lt;br/&amp;gt;nginx, serves subject-key JWKS over TLS&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    JB &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; K1 &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;K2 &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;KM &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;K9&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    K1 &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;K2 &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;KM &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;K9 &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;token exchange&amp;lt;br/&amp;gt;POST /as/token.oauth2&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; SVC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    SVC &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; ENG1 &lt;/span&gt;&lt;span&gt;&amp;amp; &lt;/span&gt;&lt;span&gt;ENG2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ENG1 &lt;/span&gt;&lt;span&gt;-.-&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;JWKS fetch&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; JWKS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ENG2 &lt;/span&gt;&lt;span&gt;-.-&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;JWKS fetch&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; JWKS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ADM &lt;/span&gt;&lt;span&gt;-.-&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;config replication&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; ENG1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ADM &lt;/span&gt;&lt;span&gt;-.-&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;config replication&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; ENG2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k6 agent Pods&lt;/strong&gt;: The load generators generate a new JWT subject token at every iteration and exchange it exactly once — no token is reused, no two requests carry the same token. The 10 Pods simulate 10 independent agent instances acting on behalf of one pool of 100 synthetic user identities, rotating round-robin. In PingFederate each Pod authenticates as its own OAuth client — &lt;code&gt;perf-agent-0&lt;/code&gt; through &lt;code&gt;perf-agent-9&lt;/code&gt; — and the subject token’s audience is the calling agent’s client id.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes Service&lt;/strong&gt;: Load-balances across both engine Pods on port 9031; admin traffic is not part of the test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingFederate engines&lt;/strong&gt;: Run the measured path — JWKS fetch and caching, JWT validation, token-exchange policy, output-token signing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingFederate admin&lt;/strong&gt;: Configures the engines through cluster replication and stays idle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;perf-jwks-server&lt;/strong&gt;: A small nginx Pod serving the subject-token verification JWKS over HTTPS (self-signed cert imported into the engines’ JVM truststore); the engines fetch and cache it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each exchange is an RFC 8693 request where the clients authenticate with &lt;code&gt;client_secret_basic&lt;/code&gt; (no actor token) and exchange a self-contained JWT subject token. The engines authenticate the client, validate the subject token’s signature (using the keys obtained from the JWKS pod), the issuer, the audience, and the expiry and issue a new token.&lt;/p&gt;
&lt;p&gt;I deliberately excluded:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User authentication (subject tokens are self-signed by the generator — no IdP round-trip)&lt;/li&gt;
&lt;li&gt;Persistent-grant storage (the client is grant-only &lt;code&gt;TOKEN_EXCHANGE&lt;/code&gt;, so the store is never touched)&lt;/li&gt;
&lt;li&gt;Any external policy calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Token exchange&lt;/h2&gt;
&lt;p&gt;Here is one measured exchange — every k6 iteration performs exactly this (secrets are redacted and tokens are truncated).&lt;/p&gt;
&lt;p&gt;Request (the subject token is signed by the load generator, RS256):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; /as/token.oauth2 HTTP/2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Host&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; pf-pingfederate-engine:9031&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Basic cGVyZi1hZ2VudC0zOjxwZXJmX2NsaWVudC1zZWNyZXQ+&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; application/x-www-form-urlencoded&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;subject_token=&lt;/span&gt;&lt;span&gt;eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIs…[611 chars]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;subject_token_type=&lt;/span&gt;&lt;span&gt;urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;requested_token_type=&lt;/span&gt;&lt;span&gt;urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;resource=&lt;/span&gt;&lt;span&gt;https%3A%2F%2Fmcp.example.internal%2Fmcp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;scope=&lt;/span&gt;&lt;span&gt;mcp%3Atools%3Ainvoke&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The subject token’s claims (decoded):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://pf-perf-subject&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;perf-user-042&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;perf-agent-3&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iat&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1789978138&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;exp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1789978438&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;jti&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;mcp-final-1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issued access token is an RS256 &lt;code&gt;at+jwt&lt;/code&gt; signed with PingFederate’s centralized signing key. Header (decoded):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;alg&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;kid&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;ejOM-MOWkc272nB1UwDyUGPasY4_RS256&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;pi.atm&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;8dzf&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;typ&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;at+jwt&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Payload (decoded):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;mcp:tools:invoke&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;authorization_details&quot;&lt;/span&gt;&lt;span&gt;: [],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;client_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;perf-agent-3&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://pf-pingfederate-engine&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iat&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1789978138&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;perf-user-042&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://mcp.example.internal/mcp&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;perf-agent-3&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;exp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1789978438&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sub&lt;/code&gt; is the subject user, taken from the subject token’s &lt;code&gt;sub&lt;/code&gt; through the token-exchange processor policy; &lt;code&gt;act.sub&lt;/code&gt; is the &lt;strong&gt;authenticated client id&lt;/strong&gt;, built by the access-token mapping from &lt;code&gt;context.ClientId&lt;/code&gt;; &lt;code&gt;aud&lt;/code&gt; is the &lt;strong&gt;requested resource URI&lt;/strong&gt;, so the token is audience-restricted to the target MCP server; &lt;code&gt;scope&lt;/code&gt; carries the requested scope, registered in the OAuth server settings — here &lt;code&gt;mcp:tools:invoke&lt;/code&gt;; &lt;code&gt;exp - iat&lt;/code&gt; is the token manager’s 5-minute lifetime.&lt;/p&gt;
&lt;p&gt;The PingFederate side is fully declarative: a server profile carries all required objects as bulk-config JSON that the image imports at startup, and both engines are guaranteed identical configuration through cluster replication.&lt;/p&gt;
&lt;h2&gt;Test stages and environment&lt;/h2&gt;
&lt;p&gt;The campaign ran three reference stages — 100, 250, and 500 exchanges per second — and repeated the whole ascending ladder three times (100 → 250 → 500, ×3) against the same continuously warmed engines. Each run is five minutes at a constant arrival rate with a 30-second warmup excluded from the measured series; a throwaway sanity run before the first round is discarded by protocol, so every counted run starts from the same warmed state.&lt;/p&gt;
&lt;p&gt;Each agent Pod drives total-rate/10 arrivals per second — at the 500 stage, 10 Pods × 50 = 500. The constant-arrival-rate executor opens each request on schedule regardless of response time, so it measures latency under controlled arrival, not throughput at saturation. All nine runs delivered their configured rates with zero dropped iterations and a 100% success rate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cluster&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Amazon EKS, region eu-west-1, Kubernetes 1.35 (EKS build)&lt;/li&gt;
&lt;li&gt;Six worker nodes, Amazon Linux 2023&lt;/li&gt;
&lt;li&gt;All nodes r5.xlarge: 4 vCPU and 32 GiB each (roughly 24 vCPU of total capacity)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;PingFederate sizing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deliberately modest, close to a small production starter:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pod&lt;/th&gt;
&lt;th&gt;CPU request&lt;/th&gt;
&lt;th&gt;CPU limit&lt;/th&gt;
&lt;th&gt;Memory request&lt;/th&gt;
&lt;th&gt;Memory limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pf-admin (1)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 Gi&lt;/td&gt;
&lt;td&gt;4 Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pf-engine (2)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3 Gi&lt;/td&gt;
&lt;td&gt;6 Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Kubernetes scheduler is configured to place the two engines on different nodes when it can.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Load generation&lt;/strong&gt;: 10 k6 Pods requesting 100m each.&lt;/p&gt;
&lt;h2&gt;How the test is run&lt;/h2&gt;
&lt;p&gt;Everything is driven by the following scripts, run from the client machine (my laptop):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployment: Installs the stack into Kubernetes&lt;/li&gt;
&lt;li&gt;Verification: Checks the deployment is healthy and the engines landed on different nodes&lt;/li&gt;
&lt;li&gt;Smoke test: Performs a single real token exchange before any load, proving the chain works&lt;/li&gt;
&lt;li&gt;Run test: Runs the actual load test and collects the results&lt;/li&gt;
&lt;li&gt;Reporting: Builds the dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alongside them, the monitoring script samples Pod and node resource usage into a CSV while the test runs. The scripts are not part of the measured path: they only observe the cluster through &lt;code&gt;kubectl&lt;/code&gt; and collect results. Everything that generates or serves load runs inside Kubernetes; everything that coordinates, copies, and computes runs outside.&lt;/p&gt;
&lt;p&gt;The run itself, step by step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Before anything starts, one terminal is started to watch the cluster. Every 5 seconds it asks Kubernetes what the PingFederate Pods are consuming — CPU, memory — and writes each answer into a CSV. This runs the whole time, in the background (Monitoring).&lt;/li&gt;
&lt;li&gt;The test starts. 10 k6 Pods begin firing token-exchange requests at PingFederate, 5 minutes per run, three ascending rounds per campaign. While each Pod works, k6 writes down every single request it made — timestamp, how long it took, whether it was warmup or real measurement — into a file inside its own container.&lt;/li&gt;
&lt;li&gt;The 5 minutes end. Each k6 prints one final summary into its Pod’s log (e.g. “I sent 15,000 requests, p95 was 9 ms, everything passed.”) and then k6 quits.&lt;/li&gt;
&lt;li&gt;The k6 Pods stay alive for 2 extra minutes after k6 quits and the run-test script collects all the results from the Pods — copying each agent’s per-request file out of its container; the summaries need no copying, they are already in the logs.&lt;/li&gt;
&lt;li&gt;Finally, the reporting script combines everything: the per-request file from each agent, the printed totals from each log, and the resource samples from the CSV — all merged into one HTML dashboard on a shared time axis, which is where every number in this article comes from.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The results&lt;/h2&gt;
&lt;p&gt;Across the nine runs of the campaign (3 stages × 3 rounds): 780,072 token exchanges, 100% success, zero dropped iterations, all thresholds passed in every run. Not one failed or malformed OAuth response in the entire campaign.&lt;/p&gt;
&lt;h3&gt;Latency by stage&lt;/h3&gt;
&lt;p&gt;Median run of the three rounds per stage, measured phase only, warmup excluded. Every column comes from the same measured window, pooled across all ten agents:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Target rate&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;avg&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;p90&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100/s&lt;/td&gt;
&lt;td&gt;100.0% achieved&lt;/td&gt;
&lt;td&gt;30,008&lt;/td&gt;
&lt;td&gt;5.4 ms&lt;/td&gt;
&lt;td&gt;5.1 ms&lt;/td&gt;
&lt;td&gt;6.9 ms&lt;/td&gt;
&lt;td&gt;7.5 ms&lt;/td&gt;
&lt;td&gt;9.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;250/s&lt;/td&gt;
&lt;td&gt;100.0% achieved&lt;/td&gt;
&lt;td&gt;75,007&lt;/td&gt;
&lt;td&gt;5.2 ms&lt;/td&gt;
&lt;td&gt;4.8 ms&lt;/td&gt;
&lt;td&gt;6.6 ms&lt;/td&gt;
&lt;td&gt;7.4 ms&lt;/td&gt;
&lt;td&gt;10.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500/s&lt;/td&gt;
&lt;td&gt;100.0% achieved&lt;/td&gt;
&lt;td&gt;150,008&lt;/td&gt;
&lt;td&gt;5.9 ms&lt;/td&gt;
&lt;td&gt;5.3 ms&lt;/td&gt;
&lt;td&gt;7.7 ms&lt;/td&gt;
&lt;td&gt;9.5 ms&lt;/td&gt;
&lt;td&gt;15.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each stage’s reported row is the &lt;strong&gt;median run of its three&lt;/strong&gt;, selected by measured p95; the per-run p95s were 100/s: 7.23/7.54/7.72 ms · 250/s: 7.31/7.44/8.09 ms · 500/s: 9.03/9.51/10.01 ms — tight spreads that indicate the numbers below are not an artifact of a single lucky run.&lt;/p&gt;
&lt;p&gt;Latency remained within a narrow range: p95 ranged from 7.4 to 9.5 milliseconds between 100 and 500 exchanges per second.&lt;/p&gt;
&lt;h3&gt;Latency and CPU over time at 500 exchanges per second&lt;/h3&gt;
&lt;p&gt;The two charts below are the Report Generator’s time-series output for the 500 tps run (the first of the three rounds at that level).&lt;/p&gt;
&lt;p&gt;Response time (in ms):
&lt;img /&gt;
&lt;em&gt;Response time p90 over the five-minute run at the 500-per-second stage&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Combined CPU load of the PingFederate engines (in millicores):
&lt;img /&gt;
&lt;em&gt;Combined engine CPU over the five-minute run at the 500-per-second stage&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Combined engine CPU holds steady at about 2.5 cores.&lt;/p&gt;
&lt;h3&gt;Resource consumption&lt;/h3&gt;
&lt;p&gt;Median run of the three rounds per stage:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Engine CPU avg&lt;/th&gt;
&lt;th&gt;Engine CPU peak&lt;/th&gt;
&lt;th&gt;Engine CPU per exchange&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100/s&lt;/td&gt;
&lt;td&gt;0.56 cores&lt;/td&gt;
&lt;td&gt;0.62 cores&lt;/td&gt;
&lt;td&gt;5.6 CPU-ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;250/s&lt;/td&gt;
&lt;td&gt;1.28 cores&lt;/td&gt;
&lt;td&gt;1.36 cores&lt;/td&gt;
&lt;td&gt;5.1 CPU-ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500/s&lt;/td&gt;
&lt;td&gt;2.62 cores&lt;/td&gt;
&lt;td&gt;2.72 cores&lt;/td&gt;
&lt;td&gt;5.2 CPU-ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last column is the CPU-per-exchange figure: it is derived by dividing the aggregate CPU consumed by the PingFederate engines by the achieved exchange rate. It should therefore be interpreted as an observed CPU-efficiency metric for this workload and environment, not as a direct measurement of the incremental CPU cost of a single token exchange. Fixed JVM and platform CPU overhead is included in the figure.&lt;/p&gt;
&lt;p&gt;Memory consumption is low: the two engine Pods together sat at a flat 2.8 GiB across all stages.&lt;/p&gt;
&lt;h2&gt;Key takeaways&lt;/h2&gt;
&lt;p&gt;In this test configuration, the token-exchange step remained in the single-digit-millisecond range at p95 for most of the tested range, reaching 9.5 ms at 500 exchanges per second. In agentic workflows where LLM reasoning and multi-step execution operate on much longer timescales, this makes the measured identity hop small relative to the overall interaction.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/carbonefederico/pingfederate-token-exchange-perf&quot;&gt;pingfederate-token-exchange-perf&lt;/a&gt; — the full project: Helm charts, k6 script, monitor and report tooling, the per-run evidence files behind every number in this article, and the HTML reports for all nine runs of the 3×3 campaign.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8693&quot;&gt;OAuth 2.0 Token Exchange&lt;/a&gt; — RFC 8693.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingfederate/latest/administrators_reference_guide/pf_oauth_config.html&quot;&gt;PingFederate OAuth configuration&lt;/a&gt; — token-exchange processor policies and access token managers.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/docs/k6/latest/&quot;&gt;k6 documentation&lt;/a&gt; — constant-arrival-rate executor and thresholds.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/blog/securing-agentic-workflows-with-token-exchange-and-workload-identity/&quot;&gt;Securing agentic workflows with token exchange and workload identity&lt;/a&gt; — the identity pattern this test measures the cost of.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CqO9QvHp_ZdkO9B.webp" medium="image" type="image/webp" width="2752" height="1536"/></item><item><title>Centralizing MCP Authorization with PingAuthorize - Part 1</title><link>https://developer.pingidentity.com/blog/centralizing-mcp-authorization-with-pingauthorize-part-1/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/centralizing-mcp-authorization-with-pingauthorize-part-1/</guid><description>How to centralize dynamic authorization for MCP servers exposed by Azure APIM with PingAuthorize.</description><pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/centralizing-mcp-authorization-with-p1authorize-part-1-azure-apim/&quot;&gt;Federico Carbone’s blog site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;Large organizations often adopt multiple cloud platforms, modern AI services, and distributed applications, which leads to increasingly fragmented authorization. A single enterprise, for example, may expose MCPs through Azure, AWS, and on-premises servers.&lt;/p&gt;
&lt;p&gt;Each platform introduces its own authorization mechanism, and over time, business rules become scattered across gateways, serverless functions, middleware, and application code. The result is duplicated policies, inconsistent decisions, difficult audits, and expensive maintenance.&lt;/p&gt;
&lt;p&gt;PingAuthorize addresses this problem by separating &lt;strong&gt;policy enforcement&lt;/strong&gt; from &lt;strong&gt;policy decision making&lt;/strong&gt;. Gateways and applications remain responsible for enforcing decisions as Policy Enforcement Points (PEPs), while PingAuthorize acts as the centralized no-code Policy Decision Point (PDP).&lt;/p&gt;
&lt;p&gt;This is the first article in a series exploring how PingAuthorize can centralize authorization decisions across different platforms and MCP environments.&lt;/p&gt;
&lt;p&gt;I start with &lt;strong&gt;Azure API Management (APIM)&lt;/strong&gt; protecting an MCP server, while future articles will apply the same model to other enforcement points such as AWS AgentCore Gateway.&lt;/p&gt;
&lt;h2&gt;What problem PingAuthorize solves: sprawl across hybrid environments&lt;/h2&gt;
&lt;p&gt;As enterprises expand across cloud providers and AI platforms, these challenges emerge:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inconsistent authorization&lt;/strong&gt;: Business policies become embedded in platform-specific implementations. The same rule may be implemented differently across gateways, clouds, and applications, producing inconsistent decisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy duplication&lt;/strong&gt;: Authorization logic is copied into multiple enforcement points. Every policy change must then be implemented several times, increasing the risk of drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authorization logic in code&lt;/strong&gt;: Without a dedicated policy engine, teams implement authorization rules directly in application code or middleware. This consumes development cycles and ties every policy change to code reviews, testing, and deployments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited governance&lt;/strong&gt;: Authorization decisions are distributed across platform-specific logs, making it harder to understand why access was granted or denied and increasing the effort required for auditing and compliance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PingAuthorize solves those challenges by centralizing &lt;strong&gt;policy evaluation&lt;/strong&gt;: it evaluates the business policy and returns the authorization decision. This allows organizations to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep business authorization rules outside applications and MCP servers&lt;/li&gt;
&lt;li&gt;Update policies without redeploying APIs&lt;/li&gt;
&lt;li&gt;Use contextual and attribute-based authorization&lt;/li&gt;
&lt;li&gt;Centralize authorization decisions and audit information&lt;/li&gt;
&lt;li&gt;Reuse the same authorization model across different platforms&lt;/li&gt;
&lt;li&gt;Gain visibility and improve auditability by logging every authorization decision&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The solution&lt;/h2&gt;
&lt;p&gt;The first implementation in this series implements an APIM Policy Fragment that delegates the authorization decisions to PingAuthorize. The fragment forwards the original MCP request to the PingAuthorize Sideband API, and then either forwards the call to the MCP server or relays the authorization denial to the caller.&lt;/p&gt;
&lt;p&gt;The following diagram depicts the components in the implementation and their interactions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; LR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    C&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Agent&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Protected MCP&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    subgraph&lt;/span&gt;&lt;span&gt; AZURE&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Azure APIM&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        APIM&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Azure API Management\nMCP endpoint&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        FRAG&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;APIM Policy Fragment\nSideband PEP&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    subgraph&lt;/span&gt;&lt;span&gt; PING&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;PingAuthorize&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        SB&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Sideband API\n/sideband/request&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    C &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;MCP tools/call\n(Agent Access Token - delegated)&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; APIM&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    APIM &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; FRAG&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    FRAG &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;Sideband request\n(method, URL, headers, body)&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; SB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    SB &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;200 without response object\nPERMIT&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; FRAG&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    SB &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;200 with response object\nDENY (status + WWW-Authenticate)&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; FRAG&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    FRAG &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; APIM&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    APIM &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot;tools/call if PERMIT\n(JSON/RPC payload)&quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; MCP&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt;: Invokes MCP tools through APIM using an Agent Access Token (typically obtained via Token Exchange).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure API Management&lt;/strong&gt;: Acts as the Policy Enforcement Point (PEP). Receives the MCP request, runs the policy fragment, and either forwards the call to the backend MCP or relays the denial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIM Policy Fragment&lt;/strong&gt;: A reusable APIM policy artifact that preserves the original request, wraps it in a Sideband request envelope, calls the PingAuthorize Sideband API with a shared secret, and enforces the returned decision. It performs no OAuth flows of its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingAuthorize Sideband API&lt;/strong&gt;: Acts as the Policy Decision Point (PDP). Receives the original request, evaluates policy over the full HTTP context, and either allows the call through (no response object) or returns a complete denial response for the PEP to relay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protected MCP&lt;/strong&gt;: The protected MCP server (in our context a demo Mortgage MCP), receives requests only after APIM allows them through.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important aspect is that APIM does not contain the business authorization logic. It forwards the request context required for policy evaluation and enforces the resulting authorization decision.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope note:&lt;/strong&gt; This article focuses on the authorization integration between APIM and PingAuthorize. Token validation, token exchange, and backend MCP server security are outside the scope of this article. In a production setup, APIM could exchange the inbound token for a backend-scoped token before calling the MCP server. For simplicity, the demo MCP server is left open and no token exchanges have been configured in APIM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Sideband integration model&lt;/h2&gt;
&lt;p&gt;With this model, APIM constructs a Sideband representation of the incoming request containing the HTTP context required by PingAuthorize. PingAuthorize evaluates policy against the full HTTP context: request method, URL, headers (including the &lt;code&gt;Authorization&lt;/code&gt; bearer token and its claims), query parameters, client IP, and the request body.&lt;/p&gt;
&lt;p&gt;The integration follows three rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Permit&lt;/strong&gt;: The PDP returns HTTP 200 with no top-level &lt;code&gt;response&lt;/code&gt; object. The PEP continues to the backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny&lt;/strong&gt;: The PDP returns HTTP 200 with a top-level &lt;code&gt;response&lt;/code&gt; object containing a complete response to hand back to the client: status code, reason, headers (including &lt;code&gt;WWW-Authenticate&lt;/code&gt;), and body. The status in that object is the PDP’s choice, not the PEP’s: &lt;code&gt;401&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header when the token is invalid or a step-up is required, &lt;code&gt;403&lt;/code&gt; when the request is authenticated but not authorized. The PEP relays it verbatim.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fail closed&lt;/strong&gt;: Any transport failure or non-200 result is an integration error, not an authorization decision. The PEP does not continue and returns &lt;code&gt;502 Bad Gateway&lt;/code&gt; (&lt;code&gt;sideband-unavailable&lt;/code&gt; for a failed call, &lt;code&gt;sideband-error&lt;/code&gt; for a non-200 reply) — an unreachable PDP never becomes implicit access.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For an MCP call such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;jsonrpc&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;tools/call&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;params&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;get_mortgage_summary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;arguments&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;customerId&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;CUST-10001&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the fragment sends to the PingAuthorize sideband endpoint the following request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;source_ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;203.0.113.10&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;source_port&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5034&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://apimid4ai.azure-api.net/mortgage-mcp/mcp/mortgage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;http_version&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;1.1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;headers&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;Accept&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;Host&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;apimid4ai.azure-api.net&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Bearer &amp;lt;agent-access-token&amp;gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;body&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;{&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;jsonrpc&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;2.0&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:2,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;tools/call&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:{&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;get_mortgage_summary&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;arguments&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:{&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;customerId&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;CUST-10001&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;}}}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A PERMIT returns HTTP 200 with no &lt;code&gt;response&lt;/code&gt; object; a DENY returns HTTP 200 with one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;response&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;response_code&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;response_status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Forbidden&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;headers&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      { &lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;body&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;{&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;jsonrpc&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;2.0&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:2,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:{&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:-32003,&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;Forbidden&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;}}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The APIM policy fragment&lt;/h2&gt;
&lt;p&gt;The integration with PingAuthorize is implemented directly as an APIM policy fragment. The fragment performs four operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Preserve the MCP request.&lt;/li&gt;
&lt;li&gt;Build the Sideband request envelope.&lt;/li&gt;
&lt;li&gt;Call the PingAuthorize Sideband API.&lt;/li&gt;
&lt;li&gt;Enforce the returned decision.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The fragment is driven by three APIM Named Values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AuthorizeSidebandRequestEndpoint&lt;/code&gt;&lt;/strong&gt; (Plain): Base URL of the Sideband API &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;/sideband/request&lt;/code&gt;.
&lt;ul&gt;
&lt;li&gt;PingAuthorize (software): &lt;code&gt;https://paz.example.com:7443&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PingOne Authorize (cloud): The gateway Service URL, such as &lt;code&gt;https://http-access-api.pingone.eu/v1/environments/{environmentId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AuthorizeSidebandClientToken&lt;/code&gt;&lt;/strong&gt; (&lt;strong&gt;Secret&lt;/strong&gt;): The sideband shared secret — the Sideband API shared secret configured on PingAuthorize, or the gateway credential on PingOne Authorize.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AuthorizeSidebandDebug&lt;/code&gt;&lt;/strong&gt; (Plain): &lt;code&gt;false&lt;/code&gt; by default. Set to &lt;code&gt;true&lt;/code&gt; temporarily to return a redacted Sideband request and the complete authorization response to the API client for diagnostics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. Preserve the MCP request&lt;/h3&gt;
&lt;p&gt;The Sideband API requires the original body as a string, so the fragment reads it before anything else and preserves it for later forwarding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;set-variable&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeOriginalRequestBody&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@(context.Request.Body == null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        ? string.Empty&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        : context.Request.Body.As&amp;lt;string&amp;gt;(preserveContent: true))&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Build the Sideband request envelope&lt;/h3&gt;
&lt;p&gt;The envelope mirrors the original request. APIM rebuilds the full request URL (scheme, host, optional port, path, query string), and collects the headers as an array of single-entry objects — the shape the Sideband API expects. The &lt;code&gt;Authorization&lt;/code&gt; header carries the agent’s bearer token unchanged, which is what lets PingAuthorize validate and inspect the caller’s token.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;set-variable&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeSidebandRequestBody&quot;&lt;/span&gt;&lt;span&gt; value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var originalUrl = context.Request.OriginalUrl;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var scheme = originalUrl.Scheme.ToLowerInvariant();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var includePort = (scheme == &quot;&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; originalUrl.Port != 80) ||&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                      (scheme == &quot;&lt;/span&gt;&lt;span&gt;https&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; originalUrl.Port != 443);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var requestUrl = originalUrl.Scheme + &quot;&lt;/span&gt;&lt;span&gt;://&lt;/span&gt;&lt;span&gt;&quot; +&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                     originalUrl.Host +&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                     (includePort ? &quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&quot; + originalUrl.Port.ToString() : string.Empty) +&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                     originalUrl.Path +&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                     originalUrl.QueryString;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var sidebandHeaders = new JArray();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sidebandHeaders.Add(new JObject(new JProperty(&quot;&lt;/span&gt;&lt;span&gt;Accept&lt;/span&gt;&lt;span&gt;&quot;, &quot;&lt;/span&gt;&lt;span&gt;application/json&lt;/span&gt;&lt;span&gt;&quot;)));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sidebandHeaders.Add(new JObject(new JProperty(&quot;&lt;/span&gt;&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;&quot;, &quot;&lt;/span&gt;&lt;span&gt;application/json&lt;/span&gt;&lt;span&gt;&quot;)));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sidebandHeaders.Add(new JObject(new JProperty(&quot;&lt;/span&gt;&lt;span&gt;Host&lt;/span&gt;&lt;span&gt;&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        originalUrl.Host +&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        (includePort ? &quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&quot; + originalUrl.Port.ToString() : string.Empty))));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sidebandHeaders.Add(new JObject(new JProperty(&quot;&lt;/span&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt;&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        context.Request.Headers.GetValueOrDefault(&quot;&lt;/span&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt;&quot;, string.Empty))));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var serializedBody =&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        (string)context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeOriginalRequestBody&lt;/span&gt;&lt;span&gt;&quot;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if (!string.IsNullOrEmpty(serializedBody))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        try&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            serializedBody = JToken.Parse(serializedBody)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .ToString(Newtonsoft.Json.Formatting.None);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        catch&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Preserve a non-JSON body exactly as received.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var payload = new JObject(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;source_ip&lt;/span&gt;&lt;span&gt;&quot;, context.Request.IpAddress ?? string.Empty),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;source_port&lt;/span&gt;&lt;span&gt;&quot;, 5034),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt;&quot;, context.Request.Method),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;&quot;, requestUrl),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;http_version&lt;/span&gt;&lt;span&gt;&quot;, &quot;&lt;/span&gt;&lt;span&gt;1.1&lt;/span&gt;&lt;span&gt;&quot;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;&quot;, sidebandHeaders),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        new JProperty(&quot;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&quot;, serializedBody)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return payload.ToString(Newtonsoft.Json.Formatting.None);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;APIM policy expressions cannot access the originating TCP source port, but the Sideband API requires a &lt;code&gt;source_port&lt;/code&gt; value between 1 and 65535. The fragment therefore uses a fixed synthetic value (&lt;code&gt;5034&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;3. Call the Sideband API&lt;/h3&gt;
&lt;p&gt;The fragment posts the envelope to &lt;code&gt;{endpoint}/sideband/request&lt;/code&gt;, authenticating with the shared secret in the &lt;code&gt;PDG-TOKEN&lt;/code&gt; header. The call is wrapped in an APIM &lt;code&gt;retry&lt;/code&gt; policy because APIM pools keep-alive connections to the Sideband endpoint, and a pooled socket that the server has already closed fails instantly — under rapid traffic this surfaced as intermittent &lt;code&gt;502&lt;/code&gt; responses even though PingAuthorize was healthy. The decision probe is idempotent (a pure policy evaluation with no side effects), so retrying is safe: the first retry is immediate, the second follows a 1-second gap:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;retry&lt;/span&gt;&lt;span&gt; condition&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@(context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandResponse&lt;/span&gt;&lt;span&gt;&quot;] == null)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;       count&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;       interval&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;       first-fast-retry&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;send-request&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        mode&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;new&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        response-variable-name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeSidebandResponse&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;20&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        ignore-error&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-url&lt;/span&gt;&lt;span&gt;&amp;gt;@(&quot;{{AuthorizeSidebandRequestEndpoint}}&quot;.TrimEnd(&apos;/&apos;) + &quot;/sideband/request&quot;)&amp;lt;/&lt;/span&gt;&lt;span&gt;set-url&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-method&lt;/span&gt;&lt;span&gt;&amp;gt;POST&amp;lt;/&lt;/span&gt;&lt;span&gt;set-method&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;PDG-TOKEN&quot;&lt;/span&gt;&lt;span&gt; exists-action&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;override&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;{{AuthorizeSidebandClientToken}}&amp;lt;/&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;/&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt; exists-action&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;override&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;application/json&amp;lt;/&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;/&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Accept&quot;&lt;/span&gt;&lt;span&gt; exists-action&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;override&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;application/json&amp;lt;/&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;/&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;set-body&lt;/span&gt;&lt;span&gt;&amp;gt;@((string)context.Variables[&quot;authorizeSidebandRequestBody&quot;])&amp;lt;/&lt;/span&gt;&lt;span&gt;set-body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/&lt;/span&gt;&lt;span&gt;send-request&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;retry&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The credential header name is part of the Sideband API configuration, so it must match what your PingAuthorize Sideband API endpoint expects — &lt;code&gt;PDG-TOKEN&lt;/code&gt; here.&lt;/p&gt;
&lt;h3&gt;4. Enforce the decision&lt;/h3&gt;
&lt;p&gt;First, fail closed on transport problems. If every retry fails, the response variable stays empty:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;choose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;when&lt;/span&gt;&lt;span&gt; condition&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@(!context.Variables.ContainsKey(&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandResponse&lt;/span&gt;&lt;span&gt;&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        || context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandResponse&lt;/span&gt;&lt;span&gt;&quot;] == null)&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;&lt;/span&gt;&lt;span&gt;return-response&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;set-status&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;502&quot;&lt;/span&gt;&lt;span&gt; reason&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Bad Gateway&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt; exists-action&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;override&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &amp;lt;&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;application/json&amp;lt;/&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;/&lt;/span&gt;&lt;span&gt;set-header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &amp;lt;&lt;/span&gt;&lt;span&gt;set-body&lt;/span&gt;&lt;span&gt;&amp;gt;@{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                return new JObject(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    new JProperty(&quot;error&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        new JObject(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            new JProperty(&quot;code&quot;, &quot;sideband-unavailable&quot;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            new JProperty(&quot;message&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                &quot;Authorization Sideband service is unavailable&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                ).ToString();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&amp;lt;/&lt;/span&gt;&lt;span&gt;set-body&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;/&lt;/span&gt;&lt;span&gt;return-response&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/&lt;/span&gt;&lt;span&gt;when&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;choose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A non-200 status is also an integration error — not an authorization denial — and returns &lt;code&gt;502&lt;/code&gt; with a &lt;code&gt;sideband-error&lt;/code&gt; code. With HTTP 200, the semantics are simple: a top-level &lt;code&gt;response&lt;/code&gt; object means DENY; its absence means PERMIT.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;set-variable&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeSidebandBody&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@(((IResponse)context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandResponse&lt;/span&gt;&lt;span&gt;&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .Body.As&amp;lt;JObject&amp;gt;(preserveContent: true))&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;choose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;when&lt;/span&gt;&lt;span&gt; condition&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@(((JObject)context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandBody&lt;/span&gt;&lt;span&gt;&quot;])[&quot;&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;&quot;] is JObject)&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;!-- DENY: extract the relay fields from the response object --&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/&lt;/span&gt;&lt;span&gt;when&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;choose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;!-- No response object means PERMIT; continue to the backend unchanged. --&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On denial, the fragment extracts &lt;code&gt;response_code&lt;/code&gt;, &lt;code&gt;response_status&lt;/code&gt;, the response headers (looking for &lt;code&gt;content-type&lt;/code&gt; and &lt;code&gt;www-authenticate&lt;/code&gt;), and the body, then relays them to the caller. The status is PingAuthorize’s decision — &lt;code&gt;401&lt;/code&gt; when the token is invalid or missing, &lt;code&gt;403&lt;/code&gt; when the caller is authenticated but not authorized (including the MCP insufficient_scope challenge):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;set-variable&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeDenyStatus&quot;&lt;/span&gt;&lt;span&gt; value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var denial = ((JObject)context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandBody&lt;/span&gt;&lt;span&gt;&quot;])[&quot;&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;&quot;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    int status;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return Int32.TryParse((string)denial[&quot;&lt;/span&gt;&lt;span&gt;response_code&lt;/span&gt;&lt;span&gt;&quot;], out status)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; status &amp;gt;= 100 &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; status &amp;lt;= 599&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            ? status&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            : 403;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;set-variable&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;authorizeDenyReason&quot;&lt;/span&gt;&lt;span&gt; value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;@{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var denial = ((JObject)context.Variables[&quot;&lt;/span&gt;&lt;span&gt;authorizeSidebandBody&lt;/span&gt;&lt;span&gt;&quot;])[&quot;&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;&quot;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return (string)denial[&quot;&lt;/span&gt;&lt;span&gt;response_status&lt;/span&gt;&lt;span&gt;&quot;] ?? &quot;&lt;/span&gt;&lt;span&gt;Forbidden&lt;/span&gt;&lt;span&gt;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the denial is returned with the authorization service’s status, reason, and &lt;code&gt;WWW-Authenticate&lt;/code&gt; header when one was supplied.&lt;/p&gt;
&lt;p&gt;The fragment also supports a temporary diagnostics mode: when &lt;code&gt;AuthorizeSidebandDebug&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, denials and integration errors return the redacted Sideband request (with &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;Cookie&lt;/code&gt;, and &lt;code&gt;Proxy-Authorization&lt;/code&gt; headers masked) and the complete authorization response to the API client, which makes endpoint and payload mistakes obvious during setup. Set it to true &lt;strong&gt;only&lt;/strong&gt; for debugging.&lt;/p&gt;
&lt;h2&gt;Policies&lt;/h2&gt;
&lt;p&gt;To test the fragment, I used a demo mortgage MCP server. In sideband mode, PingAuthorize receives the original request and exposes its full HTTP context to policies — including the JSON-RPC body. The policies extract the attributes needed for authorization evaluation (the method, the tool name, risk-relevant arguments such as &lt;code&gt;changeType&lt;/code&gt;), and the claims of the bearer token in the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;The following controls are evaluated in order (first applicable wins), after a global &lt;strong&gt;Token Validation&lt;/strong&gt; policy has already rejected expired, badly signed, or wrong-issuer tokens with a &lt;code&gt;401&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Token validity&lt;/strong&gt;: An inactive token never reaches the policy set. The global Token Validation policy denies it with a &lt;code&gt;401&lt;/code&gt; before any MCP logic runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow Delegated Token by VIP Users Only&lt;/strong&gt;: When the call is delegated (the token carries both a subject and an actor subject, &lt;code&gt;act.sub&lt;/code&gt;, meaning an agent is acting for a user), it’s permitted only if the subject is a VIP user. A standard user behind the same agent is denied with &lt;code&gt;delegation_not_permitted&lt;/code&gt;; direct, non-delegated calls are untouched by this gate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow read operations&lt;/strong&gt;: The read tools (&lt;code&gt;get_mortgage_summary&lt;/code&gt;, &lt;code&gt;calculate_affordability&lt;/code&gt;, &lt;code&gt;generate_rate_quote&lt;/code&gt;) are permitted for tokens carrying the &lt;code&gt;mortgage:read&lt;/code&gt; scope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow low risk changes&lt;/strong&gt;: &lt;code&gt;changeType = PAYMENT_DATE&lt;/code&gt; (moving a due date, in this demo defined as low risk) is permitted on the &lt;code&gt;mortgage:write&lt;/code&gt; scope with no human involvement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny High Risk Changes Without HITL&lt;/strong&gt;: Economically risky changes (&lt;code&gt;RATE_SWITCH&lt;/code&gt;, &lt;code&gt;TERM_CHANGE&lt;/code&gt;, &lt;code&gt;OVERPAYMENT&lt;/code&gt;) are denied with a machine-readable &lt;code&gt;approval_required&lt;/code&gt; challenge unless the token carries a matching approval. Nothing is executed; the denial &lt;em&gt;is&lt;/em&gt; the challenge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow High Risk Changes with HITL&lt;/strong&gt;: A risky change is permitted when the token carries an approval whose transaction context mirrors this exact payload, claim by claim, and whose &lt;code&gt;txn&lt;/code&gt; claim matches the transaction id in the payload (validated with the HITL mechanism described below).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default Deny&lt;/strong&gt;: anything else: Unknown tools, wrong scopes, anything unmatched.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Scopes act as capability classes (&lt;code&gt;mortgage:read&lt;/code&gt;, &lt;code&gt;mortgage:write&lt;/code&gt;), so adding a tool never requires reconfiguring scopes. Risk is derived from the tool call payload, not the tool: the same &lt;code&gt;submit_mortgage_change_request&lt;/code&gt; call results in permit and challenge based on its &lt;code&gt;changeType&lt;/code&gt; argument.&lt;/p&gt;
&lt;p&gt;When a policy denies a request because the token lacks the required scope, the denial is MCP-compliant. PingAuthorize composes the challenge defined by the &lt;a href=&quot;https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization&quot;&gt;MCP authorization specification&lt;/a&gt;: HTTP &lt;code&gt;403&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header carrying &lt;code&gt;error=&quot;insufficient_scope&quot;&lt;/code&gt;, the required &lt;code&gt;scope&lt;/code&gt;, and the &lt;code&gt;resource_metadata&lt;/code&gt; URL of the Protected Resource Metadata document that the MCP server exposes. The fragment relays the decision, and a compliant client parses the challenge, discovers the authorization server through the metadata document, and can run a step-up authorization for a token with the missing scope.&lt;/p&gt;
&lt;p&gt;For example, a token carrying only &lt;code&gt;mortgage:read&lt;/code&gt; calling &lt;code&gt;submit_mortgage_change_request&lt;/code&gt; triggers the challenge:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;HTTP&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;1.1&lt;/span&gt;&lt;span&gt; 403&lt;/span&gt;&lt;span&gt; Forbidden&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WWW-Authenticate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Bearer error=&quot;insufficient_scope&quot;, scope=&quot;mortgage:write&quot;, resource_metadata=&quot;https://apimid4ai.azure-api.net/.well-known/oauth-protected-resource/mortgage-mcp/mcp/mortgage&quot;, error_description=&quot;This operation requires the mortgage:write scope&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; application/json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;jsonrpc&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;error&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;code&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;-32003&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;message&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Forbidden&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The human-in-the-loop path works as a deny-then-challenge loop. A PDP decision is synchronous — it cannot pause and wait for a human — so a risky change is first denied with the &lt;code&gt;approval_required&lt;/code&gt; challenge. The human is redirected by the agent to the authorization server to obtain a short-lived transaction token whose claims mirror the approved transaction exactly (with the transaction context carried inside the access token itself):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;span&gt; standard&lt;/span&gt;&lt;span&gt; claims&lt;/span&gt;&lt;span&gt; ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;approved_for&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;submit_mortgage_change_request&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;tctx&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;tool&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;submit_mortgage_change_request&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;changeType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;TERM_CHANGE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;mortgageId&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;MORT-90001&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;requestedValue&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;30 years&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;txn&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;TXN-8f3c1a90-4d2e-4b7a-9c1f-2e5d6a8b3c4d&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;txn&lt;/code&gt; claim is the transaction identifier minted by the authorization server. When the agent retries the identical call with that token, the approval policy validates the HITL by comparing every &lt;code&gt;tctx&lt;/code&gt; claim against the parsed payload, attribute to attribute (the mirror):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tctx.tool&lt;/code&gt; against the MCP tool name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tctx.changeType&lt;/code&gt; against the parsed change type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tctx.mortgageId&lt;/code&gt; against the mortgage in the request&lt;/li&gt;
&lt;li&gt;&lt;code&gt;txn&lt;/code&gt; in the token against the transaction id in the payload&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any drift — a different change type, a different mortgage, a mismatched transaction id — breaks the mirror and the call is denied again.&lt;/p&gt;
&lt;p&gt;The transaction id included in the payload and in the token helps the backend to protect against token replays: the backend records the first execution against the transaction id and, for any subsequent request with the same id, returns the recorded outcome without re-executing the business logic.&lt;/p&gt;
&lt;p&gt;The following picture shows the policy set in the policy designer:
&lt;img /&gt;
&lt;em&gt;The mortgage MCP policy set in the PingAuthorize policy designer&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The following two pictures show how the Decision Visualizer depicts the evaluation: a PERMIT (a read tool permitted by the &lt;code&gt;mortgage:read&lt;/code&gt; scope), and a DENY (a risky change denied for missing approval):
&lt;img /&gt;
&lt;em&gt;A read tool permitted by the mortgage:read scope&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A risky change denied for missing approval&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Key takeaways&lt;/h2&gt;
&lt;p&gt;This article explained how PingAuthorize provides centralized dynamic authorization for Azure API Management. APIM acts as a Policy Enforcement Point for MCP servers without embedding business authorization rules in the gateway or in the protected MCP servers themselves. The policy fragment is the only integration artifact needed, and because it speaks the Sideband API — shared between PingAuthorize software and PingOne Authorize in the cloud — the same artifact enforces the same policies against either deployment.&lt;/p&gt;
&lt;p&gt;This pattern is most useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple teams or platforms maintain separate authorization policies that express the same business rules, making drift and inconsistency inevitable.&lt;/li&gt;
&lt;li&gt;Policy changes require coordinated redeployments across gateways, functions, or application code.&lt;/li&gt;
&lt;li&gt;Compliance or audit requirements need a centralized, queryable record of every authorization decision regardless of where it was enforced.&lt;/li&gt;
&lt;li&gt;Authorization rules depend on contextual or dynamic attributes — user roles, risk scores, time constraints — that must evolve independently of the applications that enforce them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next articles will apply the same pattern to additional enforcement surfaces such as AWS AgentCore Gateway, further showing how authorization decisions can be centralized and standardized across platforms.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingauthorize/11.1/pingauthorize_server_administration_guide/paz_about_sideband_api.html&quot;&gt;PingAuthorize Sideband API&lt;/a&gt; — how the Sideband API proxies authorization decisions.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingauthorize/11.1/pingauthorize_server_administration_guide/paz_sideband_api_config.html&quot;&gt;Sideband API configuration&lt;/a&gt; — configuring Sideband API endpoints and services on PingAuthorize.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/api-management/api-management-policies&quot;&gt;Azure API Management policies&lt;/a&gt; — reference for APIM inbound policy expressions and &lt;code&gt;send-request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/carbonefederico/ai-mcp-gateways-paz-integrations&quot;&gt;Source code&lt;/a&gt; — APIM policy fragment and configuration guidelines, including the mortgage policy set and its end-to-end test suite (&lt;code&gt;azure-apim/test/policy-e2e-tests.sh&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.BwpQLn2g_1bcIk9.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Decentralized Identity on iOS: What Happens When Your App Becomes a Digital Wallet</title><link>https://developer.pingidentity.com/blog/decentralized-identity-on-ios-when-your-app-becomes-a-digital-wallet/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/decentralized-identity-on-ios-when-your-app-becomes-a-digital-wallet/</guid><description>A step-by-step walkthrough of integrating the PingOne Wallet SDK for iOS, covering pairing, credential issuance, and handling an OpenID4VP presentation request.</description><pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;A QR code, a camera, and a question&lt;/h2&gt;
&lt;p&gt;You’ve seen the flow, maybe without thinking about it: a kiosk or a web page shows a QR code, the user opens a mobile app, points the camera at it, and a moment later the app asks, “Do you want to share proof of employment with this organization?”&lt;/p&gt;
&lt;p&gt;That moment looks like magic, but it is a protocol conversation. Inside those few seconds, a verifier has stated exactly what it needs, the user’s device has found a credential that satisfies the request, the user has decided to share it, and a signed response has gone back for verification.&lt;/p&gt;
&lt;p&gt;This article walks through that conversation from the side developers actually ship: an iOS application that &lt;em&gt;is&lt;/em&gt; the wallet. We’ll look at what a digital wallet really is under the hood, and then integrate the PingOne Wallet SDK for iOS step by step — pairing, credential issuance, and handling an OpenID4VP presentation request. By the end, the QR-code moment should feel less like magic and more like a state machine you can debug.&lt;/p&gt;
&lt;h2&gt;Decentralized identity: Proof moves into the user’s hands&lt;/h2&gt;
&lt;p&gt;For most of the history of digital identity, proof lived on servers. A login produces a token; a token is checked by another server. The user never holds anything — they hold a session at best. Every “proof of who you are” depended on a central party being online and agreeing to vouch for you, again and again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decentralized identity&lt;/strong&gt; inverts this. Trust no longer flows through a central service at verification time; it is carried in cryptographically signed data that the user holds. Two building blocks make that possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verifiable Credentials (VCs)&lt;/strong&gt;: A W3C standard format for tamper-evident statements — “this organization asserts this person is an employee with this title,” signed by the issuer. A credential in a wallet is a verifiable credential (or a closely related format), plus the cryptographic material that proves it belongs to the device presenting it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decentralized Identifiers (DIDs)&lt;/strong&gt;: A W3C standard for identifiers the user controls, resolvable to public keys without a phone home to a central identity provider. Wallets and issuers use DIDs as the signing and addressing layer: the issuer signs with its DID, the wallet proves possession of its DID’s private key, and the verifier checks signatures against the DID documents. The iOS SDK this article uses ships a DID SDK for exactly this layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Put together, the model looks like this: the user’s device holds a &lt;strong&gt;credential&lt;/strong&gt; — issued once by an organization the user trusts — and presents it to whoever needs verification, when it is needed. Ping’s framing puts it well: a credential is like a long-lived SAML assertion or OIDC ID token that doesn’t have to be reissued on every sign-on. An “Employee” credential might carry your employer and job title; an application verifies it and acts on what it says.&lt;/p&gt;
&lt;p&gt;Three roles make this work, and it is worth naming them because they map to real products:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Issuer&lt;/strong&gt;: The organization that vouches for the claims and signs the credential (for example, an employer issuing an Employee credential).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Holder&lt;/strong&gt;: The user, whose wallet app stores and guards the credential.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifier&lt;/strong&gt;: The application or service that requests and checks the credential.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; LR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    I&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Issuer&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    H&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Holder\n(wallet)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    V&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Verifier&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    I &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &quot;1. issues &amp;amp; signs credential&quot; &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; H&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    H &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;presents proof &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;when asked&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&quot; &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; V&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    V &lt;/span&gt;&lt;span&gt;-.&lt;/span&gt;&lt;span&gt; &quot;3. checks signature against issuer DID&quot; &lt;/span&gt;&lt;span&gt;.-&amp;gt;&lt;/span&gt;&lt;span&gt; I&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The decentralized identity trust triangle: the issuer signs once, the holder presents on demand, and the verifier checks the signature without calling the issuer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Notice what is absent: No server needs to be online to vouch for the user at step 2. The verifier can check the issuer’s signature on its own, from the credential and the public key material. That is the “decentralized” in decentralized identity — trust travels with the credential instead of being mediated live by an identity provider.&lt;/p&gt;
&lt;p&gt;The credentials move over open standards. &lt;strong&gt;OpenID4VP&lt;/strong&gt; (OpenID for Verifiable Presentations) defines how a verifier states what it needs and how the wallet answers. PingOne supports OpenID4VP-based credentials, which means a PingOne-issued credential can be verified by other standards-compliant products, along with a PingOne-proprietary format that adds capabilities inside the Ping ecosystem. Knowing which format you’re working with matters for interoperability decisions, and we’ll return to that in the limitations.&lt;/p&gt;
&lt;h2&gt;What a wallet actually is&lt;/h2&gt;
&lt;p&gt;Before the code, a mental model. A digital wallet is two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A key store&lt;/strong&gt;: Credentials are cryptographically bound to the device. On iOS this means keys live in the Secure Enclave and data in the keychain — which is also why a wallet doesn’t run on a simulator. The wallet signs challenges with these keys, which is how a verifier knows the credential is being presented by the device it was issued to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A state machine&lt;/strong&gt;: The wallet moves through a small number of states — &lt;em&gt;unpaired → paired → receiving → presenting&lt;/em&gt; — and every interesting event (a pairing request, a new credential, a verification request) is a message that arrives from the outside and pushes the machine to its next state.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That second point is the one that saves you debugging time. When your integration misbehaves, the question is almost never “which of fifty settings is wrong?” It is “which message arrived, and what state did it move us to?” The SDK’s design makes this visible, as the walkthrough shows.&lt;/p&gt;
&lt;h2&gt;The walkthrough: Integrating the Wallet SDK on iOS&lt;/h2&gt;
&lt;p&gt;We’ll build this in four stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Instantiate the wallet client and get an identity for the wallet instance.&lt;/li&gt;
&lt;li&gt;Pair the wallet with a user in a PingOne environment.&lt;/li&gt;
&lt;li&gt;Receive credentials (issuance and revocation).&lt;/li&gt;
&lt;li&gt;Handle a verification request: match, consent, present.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The snippets below are illustrative, adapted from the PingOne Wallet SDK sample app. They show the shape of the integration; the sample app in the &lt;a href=&quot;https://github.com/pingidentity/pingone-wallet-mobile-sdk-ios&quot;&gt;SDK repository&lt;/a&gt; is the complete, tested reference.&lt;/p&gt;
&lt;h3&gt;Stage 1: Instantiate the client and get the wallet’s identity&lt;/h3&gt;
&lt;p&gt;The wallet SDK instance in your app has an &lt;code&gt;applicationInstanceId&lt;/code&gt; — a UUID that uniquely identifies it on the PingOne platform. Your app creates the client with a builder, then reads that identifier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; clientBuilder &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; PingOneWalletClient.&lt;/span&gt;&lt;span&gt;Builder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;forRegion&lt;/span&gt;&lt;span&gt;: PingOneRegion.NA)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// Set a storage manager for encrypted local storage&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; let&lt;/span&gt;&lt;span&gt; storageManager &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; storageManager {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    clientBuilder.&lt;/span&gt;&lt;span&gt;setStorageManager&lt;/span&gt;&lt;span&gt;(storageManager)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;clientBuilder.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;onError&lt;/span&gt;&lt;span&gt; { error &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // handle initialization failure&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;onResult&lt;/span&gt;&lt;span&gt; { client &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // The applicationInstanceId identifies this wallet instance&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        let&lt;/span&gt;&lt;span&gt; applicationInstanceId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;getApplicationInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            forRegion&lt;/span&gt;&lt;span&gt;: PingOneRegion.NA)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Send it to a trusted backend service (next stage)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things are happening that are worth noticing. First, the builder pattern gives you async success and failure paths (&lt;code&gt;onResult&lt;/code&gt; / &lt;code&gt;onError&lt;/code&gt;) — this convention runs through the whole SDK. Second, the &lt;code&gt;applicationInstanceId&lt;/code&gt; is not a user identifier. It identifies &lt;em&gt;this wallet instance on this device&lt;/em&gt;. Connecting it to a user is the next stage, and it is deliberately not the app’s job to do that directly.&lt;/p&gt;
&lt;h3&gt;Stage 2: Pair the wallet with a user&lt;/h3&gt;
&lt;p&gt;A wallet only becomes useful when it is bound to a user in a PingOne environment. The design principle here matters: &lt;strong&gt;your mobile app should not call PingOne OAuth APIs directly&lt;/strong&gt;. Instead, the app sends the &lt;code&gt;applicationInstanceId&lt;/code&gt; to a backend service it already trusts, and the backend performs the pairing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your app authenticates the user with whatever mechanism it already uses.&lt;/li&gt;
&lt;li&gt;The app sends the &lt;code&gt;applicationInstanceId&lt;/code&gt; to your trusted backend.&lt;/li&gt;
&lt;li&gt;The backend calls the PingOne API to create a digital wallet for the user, passing the &lt;code&gt;applicationInstanceId&lt;/code&gt; in the request body.&lt;/li&gt;
&lt;li&gt;PingOne sends a pairing request to the wallet.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The pairing request itself is a nice example of the security model. It arrives as an end-to-end encrypted message containing a cryptographically random challenge string. The wallet signs the challenge with its device keys and returns the signature — proving, without any shared secret having crossed the network, that the request reached the device it was meant for.&lt;/p&gt;
&lt;p&gt;Your app learns about the pairing request through a callback:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; handleCredentialRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; presentationRequest: PresentationRequest) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    guard&lt;/span&gt;&lt;span&gt; let&lt;/span&gt;&lt;span&gt; pairingRequest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; presentationRequest.&lt;/span&gt;&lt;span&gt;getPairingRequest&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; // not a pairing request; handle as a presentation (Stage 4)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    pingoneWalletClient.&lt;/span&gt;&lt;span&gt;pairWallet&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt;: pairingRequest)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .&lt;/span&gt;&lt;span&gt;onResult&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // wallet paired with the user&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .&lt;/span&gt;&lt;span&gt;onError&lt;/span&gt;&lt;span&gt; { err &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // pairing failed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here on, the wallet is &lt;em&gt;paired&lt;/em&gt;: it is bound to a user and ready to hold credentials.&lt;/p&gt;
&lt;h3&gt;Stage 3: Receive credentials&lt;/h3&gt;
&lt;p&gt;Credentials reach the wallet over the PingOne Mailbox service, and the delivery model explains most issuance questions before they become support tickets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a user’s attributes change, PingOne’s credential lifecycle manager evaluates whether a credential should be issued or revoked, stages the change, and a periodic process issues or revokes accordingly. &lt;strong&gt;Issuance is scheduled, not real-time&lt;/strong&gt; — if you’re testing and don’t see a credential arrive, check whether the next processing cycle has run.&lt;/li&gt;
&lt;li&gt;Issued and revoked credentials travel to the wallet as end-to-end encrypted mailbox messages.&lt;/li&gt;
&lt;li&gt;The SDK polls the mailbox (every 3 seconds by default) and calls your app through the callback handler when something arrives.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The callbacks mirror the lifecycle:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; handleCredentialIssuance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;issuer&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                              message&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                              challenge&lt;/span&gt;&lt;span&gt;: Challenge&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                              claim&lt;/span&gt;&lt;span&gt;: Claim,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                              errors&lt;/span&gt;&lt;span&gt;: [PingOneWallet.WalletException]) &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; Bool&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // Persist the credential to your secure storage&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    dataRepository.&lt;/span&gt;&lt;span&gt;saveCredential&lt;/span&gt;&lt;span&gt;(claim)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; handleCredentialRevocation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;issuer&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                message&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                challenge&lt;/span&gt;&lt;span&gt;: Challenge&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                claimReference&lt;/span&gt;&lt;span&gt;: ClaimReference,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                errors&lt;/span&gt;&lt;span&gt;: [PingOneWallet.WalletException]) &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; Bool&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // Remove or mark the revoked credential&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    dataRepository.&lt;/span&gt;&lt;span&gt;saveCredentialReference&lt;/span&gt;&lt;span&gt;(claimReference)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note what the SDK does &lt;em&gt;not&lt;/em&gt; do here: it doesn’t decide where credentials live in your app. Storage is yours — the sample app uses a &lt;code&gt;DataRepository&lt;/code&gt; backed by Secure Enclave keys, and the protocol it implements is designed to be replaced with your own implementation.&lt;/p&gt;
&lt;h3&gt;Stage 4: The verification request — the heart of the flow&lt;/h3&gt;
&lt;p&gt;Now the QR-code moment. A verifier shows a code (or sends a deep link, or a push notification); the SDK parses it, fetches the presentation request, and calls the same &lt;code&gt;handleCredentialRequest&lt;/code&gt; callback you saw in Stage 2. This time it is not a pairing request, and your job is to answer three questions in order:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is being asked?&lt;/strong&gt; The presentation request describes the claims the verifier needs — a job title, an employment status, a date of birth — in the language of the OpenID4VP presentation exchange.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do we have it?&lt;/strong&gt; Matching happens locally, on the device, before the user is involved:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; matcherResults &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; pingoneWalletClient&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;findMatchingCredentialsForRequest&lt;/span&gt;&lt;span&gt;(presentationRequest)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;getResult&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; matchingCredentials &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; matcherResults.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;$0&lt;/span&gt;&lt;span&gt;.claims.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;guard&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt;matchingCredentials.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt; else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // Tell the user what is missing rather than failing silently&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ordering is deliberate and it is the privacy core of the design: the request is evaluated against the wallet’s contents on-device. Only after a match exists does the user see anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does the user consent?&lt;/strong&gt; This is your app’s moment. If multiple credentials match, the app lets the user choose. Then the app presents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pingoneWalletClient.&lt;/span&gt;&lt;span&gt;presentCredentials&lt;/span&gt;&lt;span&gt;(credentialPresentation)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;onResult&lt;/span&gt;&lt;span&gt; { result &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        switch&lt;/span&gt;&lt;span&gt; result.&lt;/span&gt;&lt;span&gt;getPresentationStatus&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        case&lt;/span&gt;&lt;span&gt; .success&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Verified. The verifier accepted the presentation.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        case&lt;/span&gt;&lt;span&gt; .failure&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // The presentation was rejected&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        case&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;span&gt;requiresAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; action)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // The verifier sent a follow-up action to handle&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;onError&lt;/span&gt;&lt;span&gt; { err &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // transport-level failure&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The three-way status is worth designing your UI around. &lt;code&gt;success&lt;/code&gt; closes the loop; &lt;code&gt;failure&lt;/code&gt; usually means the verifier’s policy rejected what was presented; &lt;code&gt;requiresAction&lt;/code&gt; means the conversation isn’t over — the verifier has replied with another step the wallet needs to perform.&lt;/p&gt;
&lt;h2&gt;The whole flow on one page&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; V&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Verifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; P&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; PingOne&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; W&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Wallet app (your iOS app)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    V&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; QR code / deep link&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Presentation request (encrypted mailbox)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Match credentials (on-device)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; User selects + consents&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Signed presentation&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The verification flow end to end: the request arrives over the encrypted mailbox, matching and consent happen on-device, and only the signed presentation leaves the wallet.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Pairing and issuance follow the same mailbox pattern: a message arrives encrypted, the SDK decrypts and verifies it, the app gets a callback, and the user’s data stays on the device until the user acts.&lt;/p&gt;
&lt;h2&gt;Limitations and what to know before you build&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Device, not simulator&lt;/strong&gt;: The wallet uses the Secure Enclave, the keychain, and (in the sample app) the camera for profile creation. Plan for on-device testing from day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled issuance&lt;/strong&gt;: Credential issue and revoke run on a processing schedule, not instantly. When your test credential doesn’t appear and you’re sure the polling is fine, the schedule is the suspect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two credential formats&lt;/strong&gt;: OpenID4VP-based credentials interoperate with other standards-compliant verifiers; PingOne’s proprietary format offers additional capabilities within the Ping ecosystem. Choose deliberately, because it decides who can verify what you issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifier-side setup&lt;/strong&gt;: A verifying environment needs an issuer profile configured before it can verify credentials — a detail that surprises teams testing cross-environment verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The snippets are illustrative&lt;/strong&gt;: They are adapted from the sample app and simplified for length. Treat the sample app as the source of truth, and validate against the product documentation for your SDK version.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A digital credential is a long-lived, signed set of claims the &lt;em&gt;user&lt;/em&gt; holds — the decentralized identity answer to “prove this about me” without a fresh token exchange every time.&lt;/li&gt;
&lt;li&gt;A wallet is a key store plus a state machine. Debugging your integration means asking which message arrived and what state it produced.&lt;/li&gt;
&lt;li&gt;Integration has exactly four stages: instantiate and get the wallet’s identity, pair through a trusted backend, receive lifecycle events via encrypted mailbox messages, and answer presentation requests with match → consent → present.&lt;/li&gt;
&lt;li&gt;Matching happens on-device before consent. That ordering is not an implementation detail — it is the privacy property that makes the pattern worth adopting.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where to go next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Explore the &lt;a href=&quot;https://github.com/pingidentity/pingone-wallet-mobile-sdk-ios&quot;&gt;PingOne Wallet SDK for iOS&lt;/a&gt; sample app, which contains the complete, runnable version of every snippet in this article.&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://docs.pingidentity.com/pingone/digital_credentials_using_pingone_credentials/p1_credentials_getting_started.html&quot;&gt;PingOne Credentials product documentation&lt;/a&gt; for issuance rules, credential formats, and verifier configuration.&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://openid.net/specs/openid-4-verifiable-presentations-1_0.html&quot;&gt;OpenID4VP specification&lt;/a&gt; if you want to understand what is actually inside a presentation request.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.gByJfcvf_ZFtYJU.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Login Widget Customization, from CLI to Chat Prompt</title><link>https://developer.pingidentity.com/blog/from-hand-rolled-custom-components-to-ai-assisted-scaffolding/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/from-hand-rolled-custom-components-to-ai-assisted-scaffolding/</guid><description>A firsthand look at how building custom login UI on the Ping Login Framework has evolved, from hand-written Svelte components to CLI scaffolding to AI-assisted generation via MCP.</description><pubDate>Fri, 25 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We spend a lot of our time in the Ping Login Framework, the Svelte-based toolkit behind our Login Widget. Building a custom login screen is always the same task at heart, but lately we’ve watched our own process for doing it evolve through three distinct stages.&lt;/p&gt;
&lt;p&gt;Each stage streamlines things a bit more than the last, and by the third one we’re barely writing any code ourselves. We want to walk through that evolution, and why building each tool led us straight to building the next one.&lt;/p&gt;
&lt;p&gt;One thing up front: everything past the first section is &lt;strong&gt;experimental&lt;/strong&gt;. The APIs might change as we refine things, and none of it is officially supported yet.&lt;/p&gt;
&lt;p&gt;If you like seeing where things are headed before everyone else, though, read on.&lt;/p&gt;
&lt;h2&gt;Building custom components&lt;/h2&gt;
&lt;p&gt;The Login Framework ships with a full set of built-in Svelte components that render every stage and callback type out of the box, so most apps built with the Login Widget never need to touch any of this. But sooner or later you’ll want to break away from the defaults.&lt;/p&gt;
&lt;p&gt;Maybe you need a different look and feel: your own branding, layout, or validation messaging. Or maybe you need functionality the built-ins just don’t offer, like an extra field on a login form, or a bespoke UX for a particular callback type. That’s what custom components are for.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Implementing a custom callback component for boolean attributes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The framework supports two kinds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stage components&lt;/strong&gt;: Replace an entire form layout for a specific stage.&lt;/p&gt;
&lt;p&gt;Think a fully custom &lt;code&gt;DefaultLogin&lt;/code&gt; screen with your own branding, layout, and validation messaging.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Callback components&lt;/strong&gt;: Override how a specific callback type renders, globally, wherever it shows up in any journey.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To build either one, you drop a &lt;code&gt;.svelte&lt;/code&gt; file into &lt;code&gt;experimental/custom/stages/&lt;/code&gt; or &lt;code&gt;experimental/custom/callbacks/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It has to start with a metadata header that the framework’s Vite plugin uses to auto-register it during the build:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!--&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Type: stage&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Name: DefaultLogin&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  export&lt;/span&gt;&lt;span&gt; let&lt;/span&gt;&lt;span&gt; form, journey, step;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;!-- your custom layout --&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The framework reads that header at build time to auto-register the component, so no manual wiring needed.&lt;/p&gt;
&lt;p&gt;This is still the most powerful option. You get full control over markup, styling, and behavior, and it’s where we still land for anything with unusual design requirements.&lt;/p&gt;
&lt;h2&gt;Scaffolding with the ping-lf CLI&lt;/h2&gt;
&lt;p&gt;The thing is, the header syntax has to be exact. A typo or a missing field, and the build fails without much to tell you why.&lt;/p&gt;
&lt;p&gt;Add in the directory conventions and the prop contract for each component type, and it’s the kind of detail you tend to get right the second time, not the first.&lt;/p&gt;
&lt;p&gt;So we wrote a CLI, &lt;code&gt;ping-lf&lt;/code&gt;, to take care of the boilerplate.&lt;/p&gt;
&lt;p&gt;You can install the &lt;code&gt;ping-lf&lt;/code&gt; tool as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -g&lt;/span&gt;&lt;span&gt; @forgerock/login-framework-cli&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You must be running Node 20 for the following &lt;code&gt;ping-lf&lt;/code&gt; commands to work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After installing the CLI, you’ll have the following tools available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;/strong&gt;: Initialize a new project from a published framework release or a local framework directory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;generate callback&lt;/code&gt;&lt;/strong&gt;: Scaffold a new custom callback component under &lt;code&gt;experimental/custom/callbacks/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;generate stage&lt;/code&gt;&lt;/strong&gt;: Scaffold a new custom stage component under &lt;code&gt;experimental/custom/stages/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;releases&lt;/code&gt;&lt;/strong&gt;: List the framework releases available on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;update&lt;/code&gt;&lt;/strong&gt;: Update an existing project to a newer framework version while preserving your custom components and configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, you can initialize a new Login Widget project as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ping-lf&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span&gt; ./MyCustomWidget&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; MyCustomWidget&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you’re ready to scaffold your custom stage component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ping-lf&lt;/span&gt;&lt;span&gt; generate&lt;/span&gt;&lt;span&gt; stage&lt;/span&gt;&lt;span&gt; MyCustomLogin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one command creates the file in the right directory, with a correctly formed metadata header, matching the naming convention the framework expects.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;generate&lt;/code&gt; command also scaffolds Storybook stories, a mock journey step, utility functions, and a unit test.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Best Practice:&lt;/strong&gt; Over time you’ll want to keep your project up to date, and again the &lt;code&gt;ping-lf&lt;/code&gt; tool can help.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;ping-lf update&lt;/code&gt; to pull in framework changes while leaving your custom components and config alone.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What we like about the CLI isn’t that it saves keystrokes. It’s that it removes an entire category of mistakes.&lt;/p&gt;
&lt;p&gt;You can’t misspell &lt;code&gt;Type:&lt;/code&gt; in the header if the CLI writes it for you. You get the directory structure right by construction. It turns “read the docs carefully” into “run a command”, which is a much better place for a team to be once you’ve got more than one or two people touching custom components.&lt;/p&gt;
&lt;p&gt;Still, at this point you’re the one driving. You decide what to build, you run the command, and then you go write the actual component logic and layout by hand. The CLI just clears the path to that part.&lt;/p&gt;
&lt;h2&gt;Supercharged components: adding an MCP for use by LLMs&lt;/h2&gt;
&lt;p&gt;Once the CLI existed with a clean set of typed commands behind it, it was a short step to ask: why not expose those same commands as tools an AI assistant can call directly? So &lt;code&gt;ping-lf&lt;/code&gt; can also run as an &lt;strong&gt;MCP server&lt;/strong&gt;, the same protocol that tools like Google Gemini and Claude Code use to call out to external tools.&lt;/p&gt;
&lt;p&gt;Pass &lt;code&gt;ping-lf&lt;/code&gt; the &lt;code&gt;mcp&lt;/code&gt; parameter to run it in MCP mode. Point your AI assistant at it, and every one of those CLI commands becomes something the assistant can call directly, in the middle of a conversation, without you touching a terminal.&lt;/p&gt;
&lt;p&gt;Wiring it up is just a config entry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;ping-lf&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;-y&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;@forgerock/login-framework-cli&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;mcp&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After connecting to an assistant, you’ll have the exact same operations the CLI exposes, just callable directly from your assistant instead of typed as a CLI option.&lt;/p&gt;
&lt;p&gt;In practice, that means we can describe what we want in plain language, something like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Using the ping-lf MCP server, add a custom component for &lt;code&gt;BooleanAttributeInputCallback&lt;/code&gt; that has a nice toggle effect between the two options, where a pill moves left or right to highlight the option and dim the unselected one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The assistant scaffolds the component &lt;em&gt;and&lt;/em&gt; fills in a first pass at the markup, using the framework’s own conventions, because it can see and call the same tools we would.&lt;/p&gt;
&lt;p&gt;The interesting part isn’t that it saves typing. What it actually does is collapse the whole loop, by giving the LLM a declarative way to create the scaffolded code, rather than guessing at making the files directly.&lt;/p&gt;
&lt;p&gt;You describe the outcome, get a working starting point, and iterate in conversation instead of switching between docs, terminal, and editor.&lt;/p&gt;
&lt;p&gt;It doesn’t replace the first section of this post. You still need to understand stages and callbacks to know what to ask for, but you can focus on the result you want, rather than the details of how to build it.&lt;/p&gt;
&lt;p&gt;It turns the CLI usage from earlier into something closer to “&lt;em&gt;just tell it what you want&lt;/em&gt;”.&lt;/p&gt;
&lt;h2&gt;Where this leaves things&lt;/h2&gt;
&lt;p&gt;This is early-access, experimental tooling, and it’ll keep changing shape as we get feedback. But the trajectory is clear enough that we think it’s worth sharing now: from hand-writing metadata headers, to a CLI that writes them for you, to an AI assistant that can use that CLI on your behalf.&lt;/p&gt;
&lt;p&gt;If you’re building on the Login Framework and want to try any of this, especially the MCP piece, grab the CLI and give it a spin!&lt;/p&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/secure_login_technology.DWM0_xOw_Z2d9bV1.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Token Exchange with Delegation in the Agentic Era</title><link>https://developer.pingidentity.com/blog/token-exchange-with-delegation-in-the-agentic-era/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/token-exchange-with-delegation-in-the-agentic-era/</guid><description>A primer on OAuth Token Exchange and delegation, exploring the concepts through real-world examples and applying them to both traditional and agentic systems.</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/token-exchange-delegation-agentic/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;If you’re familiar with OAuth, you’ve likely come across &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8693&quot;&gt;&lt;strong&gt;RFC 8693 - Token Exchange&lt;/strong&gt;&lt;/a&gt;. An often-overlooked capability within the OAuth specification, token exchange gives us the ability to communicate between multiple services and applications, re-scope requested tokens, and &lt;strong&gt;delegate&lt;/strong&gt; authority to services downstream.&lt;/p&gt;
&lt;p&gt;Delegation in particular has become a hot topic as of late due to the rise of &lt;strong&gt;agentic systems&lt;/strong&gt;. With the rise of AI tooling deciding to perform actions in a non-deterministic manner, it is becoming more and more important to delegate those actions instead of giving them broad overarching permissions without auditability.&lt;/p&gt;
&lt;p&gt;If some — or all — of that went over your head, you’re not alone and you’re in the right place.&lt;/p&gt;
&lt;p&gt;This Guide aims to give you a high-level overview of what Token Exchange is, how Delegation applies, and what that means in agentic systems. We’ll do that by taking a closer look at what Delegation means in the context of application, and then agentic, identity.&lt;/p&gt;
&lt;p&gt;It’s split into the following parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-token-exchange-with-delegation&quot;&gt;Understanding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#applying-token-exchange-with-delegation&quot;&gt;Application&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By the end of this Guide you should have a high-level understanding of what Token Exchange is, why it and delegation is important, and how it applies to both an agentic and non-agentic system. You’ll also gather a deeper understanding of why identity is so important to this system.&lt;/p&gt;
&lt;h2&gt;What is token exchange with delegation?&lt;/h2&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;We use OAuth to &lt;strong&gt;secure access to resources&lt;/strong&gt;. It allows us to define &lt;strong&gt;who owns what&lt;/strong&gt; and &lt;strong&gt;what permissions they have&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;OAuth also allows us to &lt;strong&gt;share access to resources securely&lt;/strong&gt;. Given what we know about ownership and permissions, we can &lt;strong&gt;permit others access&lt;/strong&gt; — forever or temporarily (&lt;strong&gt;lifetime&lt;/strong&gt;) and with specific permissions (&lt;strong&gt;scopes&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;When we share resources, we want to be very specific about &lt;strong&gt;what we share&lt;/strong&gt; and &lt;strong&gt;with whom&lt;/strong&gt; we share that resource with. We also likely want to know, once we’ve shared, &lt;strong&gt;who is doing what and who are they doing that action for&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Token Exchange with Delegation provides us that detail.&lt;/p&gt;
&lt;p&gt;Let’s define some key players in exchange: the &lt;strong&gt;Subject&lt;/strong&gt;, the &lt;strong&gt;Actor&lt;/strong&gt;, and the &lt;strong&gt;Authorization Server&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Subject&lt;/strong&gt; is the primary entity that owns the resource. This could be an end-user, a microservice, an embedded system, or otherwise — it’s the &lt;strong&gt;primary identity&lt;/strong&gt; that this exchange is for.&lt;/li&gt;
&lt;li&gt;The Subject passes their token to the &lt;strong&gt;Actor&lt;/strong&gt;, who is the entity that exchanges the token. This could be an application, secondary microservice, or even another identity who will receive the exchanged token and (likely) perform an action &lt;strong&gt;on behalf of the Subject&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Authorization Server (AS)&lt;/strong&gt; performs the exchange. There could be more than one AS depending on if the exchange is occurring cross-domain with trusted systems or all be performed on a single AS if the Subject and Actor live in the same domain. The AS will ultimately decide what scopes the Actor is able to receive based on the request that they made.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the end of the day, the &lt;strong&gt;Actor&lt;/strong&gt; exchanges the &lt;strong&gt;Subject’s Token&lt;/strong&gt; with the &lt;strong&gt;Authorization Server&lt;/strong&gt; to get a new token modified to perform the specific task the Actor wants to take. And &lt;strong&gt;exchanges can be chained&lt;/strong&gt; — some services may call additional services, and along the way their permissions and Subject/Actor detail updates accordingly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token Exchange&lt;/strong&gt; is the process of &lt;strong&gt;transforming the scope&lt;/strong&gt;. &lt;strong&gt;Delegation&lt;/strong&gt; is the “&lt;strong&gt;on behalf of&lt;/strong&gt;” relationship tied to the exchange itself. While you can perform Token Exchange without Delegation, &lt;strong&gt;it’s critical to delegate when we want a clear audit trail of who performed the action and who they performed it for&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Example&lt;/h3&gt;
&lt;p&gt;Getting clearer? Let’s break this down with a non-technical example: paying someone from your bank account.&lt;/p&gt;
&lt;p&gt;In this example, you want to pay your friend $10. You could go to your bank, give them your ID and PIN, withdraw $10, and then give it to your friend.&lt;/p&gt;
&lt;p&gt;Let’s say you can’t get to the bank. Instead, you could give your friend your ID and PIN, have them go to the bank, and have them withdraw the $10.&lt;/p&gt;
&lt;p&gt;But wait! If they have your ID and PIN, who’s to say they won’t transfer more than $10, change your PIN, or close your account? By sharing your credentials directly you’ve given your friend far too many permissions.&lt;/p&gt;
&lt;p&gt;So instead, you issue a temporary PIN that lets your friend go to the bank and withdraw only $10. To make that PIN you had to prove yourself to your bank and because the PIN is tied to the $10 transaction its blast radius has been significantly reduced (one might even say “downscoped”). But that PIN is missing some key information — we have no idea who withdrew the $10. In fact, in the eyes of the bank, YOU withdrew it. &lt;strong&gt;We just performed Token Exchange without delegation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So let’s clear this up further. Rather than a temporary PIN you give your friend a check. That check has your bank routing number on it, the exact amount your friend will get from your account, AND (notably) your friend’s information on it. In fact, before depositing that check the friend is going to have to &lt;strong&gt;validate themselves&lt;/strong&gt; by endorsing the check and the bank will &lt;strong&gt;validate the transaction&lt;/strong&gt; before letting your friend cash it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You are the Subject, your friend is the Actor, and the bank is the Authorization Server.&lt;/strong&gt; You validated yourself by using a valid check, you have downscoped your access to withdrawal only and for a specified amount, and your friend is acting on behalf of you to withdraw using the permissions that they exchanged at the bank who approved the whole thing.&lt;/p&gt;
&lt;p&gt;In the case of chaining requests, we are going to be a little imaginative with this example.&lt;/p&gt;
&lt;p&gt;Let’s say that your friend employed a couple other people to help with your task. You don’t know who they are but you do know that you owe your friend $10. Chaining exchanges lets your friend create an invoice that pays their employees from the $10 you gave them access to in your bank account. This would let you, the bank, and your friend be able to see that your friend’s employees got paid through your friend using your money. &lt;strong&gt;A chained token exchange gives you full visibility into each actor before it interacts with the resource.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Applying token exchange with delegation&lt;/h2&gt;
&lt;p&gt;Now that we’ve a good baseline, let’s apply this to a non-agentic system and then evolve it into an agentic one.&lt;/p&gt;
&lt;h3&gt;Delegation in a non-agentic system&lt;/h3&gt;
&lt;p&gt;A common system may look like what you see here.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A basic, non-agentic system&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;user&lt;/strong&gt; uses an &lt;strong&gt;application&lt;/strong&gt; to interact with a &lt;strong&gt;service&lt;/strong&gt; or &lt;strong&gt;resource&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the most traditional sense, this could be an employee logging into a document site to edit a presentation, or a customer listing their previous orders. But an application is really an interface to the service — it could just as easily be a telephony product embedded in a helpdesk system or an embedded system receiving and interacting with analytics data relevant to the end user.&lt;/p&gt;
&lt;p&gt;At the end of the day, your infrastructure likely relies on something — be it a person, system, or otherwise — interacting with something else to receive or act upon a service.&lt;/p&gt;
&lt;p&gt;And in all reality, your application is likely communicating with more than one service. Microservice architectures, SaaS platforms, and specified tools have given rise to the implementation of an API Gateway that receives, validates, and routes requests to the appropriate service given the context of the user’s — and the application’s — request.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding an API Gateway&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So how does identity play into all of this?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Layering an Identity Provider over a non-agentic system&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Your identity layer sits on top of every stage in the flow from a user down to the resource.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;user authenticates&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;application exchanges the user’s token&lt;/strong&gt; with the appropriate scope to perform an action for the user&lt;/li&gt;
&lt;li&gt;The API Gateway &lt;strong&gt;introspects the exchanged token&lt;/strong&gt; to ensure its validity and optionally &lt;strong&gt;exchanges again&lt;/strong&gt; to provide additional context and scoping on the call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, the identity provider helps you answer the following questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Who is this request for?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who is performing the request?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;If they’re able, &lt;strong&gt;What can they do?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Questions an Identity Provider answers&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let’s break this down in more detail:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Who is this request for?&lt;/strong&gt; In this example, it’s your end-user. Not only do you want to know who this action should be performed on, but you want to ensure that they approved this action to have taken place. In our flow we have this level of assurance because your user &lt;strong&gt;authenticated&lt;/strong&gt; with their identity provider and &lt;strong&gt;initiated&lt;/strong&gt; the request with their &lt;strong&gt;valid access token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who is performing the request?&lt;/strong&gt; Your application, upon receiving the user’s token, exchanged it for their own. This tied the application to the request as the &lt;strong&gt;actor&lt;/strong&gt; — they are performing an action &lt;strong&gt;on behalf of&lt;/strong&gt; the user. Ultimately, this gives you a &lt;strong&gt;clear audit trail&lt;/strong&gt; of &lt;strong&gt;who is doing what and for who&lt;/strong&gt;. The API gateway can also exchange the request from the Application to scope the request to themselves and track that the gateway is acting not only on behalf of the application but also the user.&lt;/li&gt;
&lt;li&gt;If they’re able, &lt;strong&gt;What can they do?&lt;/strong&gt; Now that you have all of this information — the actions being requested (the scopes), the resources they want to access, the user that requested and the applications that are acting on behalf of that user, you can use an &lt;strong&gt;authorization engine&lt;/strong&gt; to &lt;strong&gt;strongly verify&lt;/strong&gt; that all of this data matches what you expect.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your identity provider plays a part in all of these interactions: It &lt;strong&gt;authenticates the user&lt;/strong&gt;, &lt;strong&gt;exchanges the tokens&lt;/strong&gt;, and &lt;strong&gt;validates the requests&lt;/strong&gt; based on the policies you’ve defined.&lt;/p&gt;
&lt;h3&gt;Delegation in an agentic system&lt;/h3&gt;
&lt;p&gt;Securing an Agentic interface isn’t too different from the concepts you just reviewed.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;An agentic system&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That same application you were using before now likely contains an interface to interact with your agent. Your agent is listening to that interface. &lt;strong&gt;The agent isn’t the reasoning engine&lt;/strong&gt;: it is a deterministic system that surfaces common, repeatable actions you want your non-deterministic system to take.&lt;/p&gt;
&lt;p&gt;That agent can be plugged into one, or many, LLMs — Large Language Models (like Claude, ChatGPT, etc.) which use the context of the user interface and the actions surfaced by the agent to make decisions based on what it is understanding and learning from.&lt;/p&gt;
&lt;p&gt;But an agent by itself isn’t particularly useful. It, like your application, needs to interact with services and resources based on the actions it’s been told to do. You could surface the raw API calls directly but there are common interfaces, like &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25&quot;&gt;MCP (Model Context Protocol)&lt;/a&gt; that have defined standards for how to surface tools for your Agent to discover and subsequently use.&lt;/p&gt;
&lt;p&gt;Just like before, you likely have more than one set of services and as such likely have more than one MCP server. To easily surface all tools, and make it simple to interact with them, you probably have an MCP Gateway to field and route those requests.&lt;/p&gt;
&lt;p&gt;And, as we saw in the first example, your identity lives across the entire operational flow. The only difference is since we’ve added an additional layer of action, we’ve added an additional layer of exchange.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Layering an Identity Provider over an agentic system&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This answers the exact same questions as before — &lt;strong&gt;Who is this request for&lt;/strong&gt;, &lt;strong&gt;Who is performing the request&lt;/strong&gt;, and if they’re able, &lt;strong&gt;What can they do?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Questions an Identity Provider answers in an agentic system&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And that token exchange, that delegation, is key to this approach — we are now tracking, auditing, and validating that the application, the downstream agent, and optionally the gateway have the appropriate permission to perform the actions they’ve been instructed to do.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;You should now have a high-level understanding of Token Exchange with Delegation, along with a view into how to apply it in both non-agentic and agentic use cases. You should understand the importance of knowing who is performing what and for whom, and that this relationship can be chained through more than one identity. Finally, you should recognize the incredible importance of identity in managing this system.&lt;/p&gt;
&lt;p&gt;This guide is your primer, 101 course into the architectural basics. With it, you can begin designing authorization strategies that utilize the information provided by the delegating parties in your identity stack.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.D0BA2Lc8_OX7lf.webp" medium="image" type="image/webp" width="2752" height="1536"/></item><item><title>Custom Node Tips, Tricks, and Utilities</title><link>https://developer.pingidentity.com/blog/custom-node-tips-tricks-utilities/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/custom-node-tips-tricks-utilities/</guid><description>Learn common Custom Node reusable patterns that make development more consistent, easier to manage, and faster to implement.</description><pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/custom-node-tips-tricks-utilities/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/journeys/node-designer.html&quot;&gt;Custom Nodes&lt;/a&gt; in PingOne Advanced Identity Cloud and PingAM enable the creation and management of flexible, reusable Journey Nodes. Unlike &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/scripted-decision.html&quot;&gt;Scripted Decision Nodes&lt;/a&gt;, custom nodes let us define input parameters and output values so that we can &lt;strong&gt;templatize&lt;/strong&gt; and &lt;strong&gt;genericize&lt;/strong&gt; common actions we find ourselves scripting (and re-scripting) over and over.&lt;/p&gt;
&lt;p&gt;As I’ve been building out Custom Nodes, I’ve collected some common reusable patterns that make development more consistent, easier to manage, and faster to implement.&lt;/p&gt;
&lt;p&gt;This guide will walk you through some tips and tricks when building Custom Nodes as well as provide you functionality to help kickstart your next Node design. It’s broken down into the following sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#when-to-use-a-custom-node&quot;&gt;When to Use a Custom Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#when-not-to-use-a-custom-node&quot;&gt;When Not to Use a Custom Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-to-structure-a-custom-node&quot;&gt;How to Structure a Custom Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#useful-functionality&quot;&gt;Useful Functionality&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#gotchas&quot;&gt;Gotchas&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This guide expects a level of familiarity with Scripting in PingAM/PingOne Advanced Identity Cloud as well as an understanding of Journeys.&lt;/p&gt;
&lt;h2&gt;When to Use a Custom Node&lt;/h2&gt;
&lt;h3&gt;Reusability and Traceability&lt;/h3&gt;
&lt;p&gt;If you find yourself using the same Scripted Decision Node across multiple Journeys, Realms, or Tenants, it may be time for you to build a Custom Node.&lt;/p&gt;
&lt;p&gt;Custom Nodes make reusing custom code significantly easier, for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom Nodes are available &lt;strong&gt;cross-realm&lt;/strong&gt; — meaning you can design and maintain the node in one place and use it across all of your PingAM realms.&lt;/li&gt;
&lt;li&gt;Custom Nodes &lt;strong&gt;detect and show usage and cannot be deleted when in use&lt;/strong&gt;. Ever accidentally deleted a script that was used in a Journey? Custom Nodes prevent that issue as well as give you broader visibility into how it’s being implemented.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sharability&lt;/h3&gt;
&lt;p&gt;If you find yourself creating custom capabilities that help your broader team, you’ll likely save a ton of time down the line if you convert your script into a Custom Node.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom Nodes &lt;strong&gt;show up in the Node selector in the Journey Canvas&lt;/strong&gt; — meaning your designers don’t have to know what script to pick or configure and can organically find your node as they build their Journeys.&lt;/li&gt;
&lt;li&gt;Custom Nodes have &lt;strong&gt;preset outcomes&lt;/strong&gt; — saving your Journey designers the step of reviewing your Scripted Decision Node code to find the right outcomes and hopefully spell them correctly in the Node configuration.&lt;/li&gt;
&lt;li&gt;Custom Nodes have &lt;strong&gt;built-in import and export utilities&lt;/strong&gt; — meaning you have a preconfigured bundle that can be stored (locally, remotely, in git, etc), tracked, and imported using both API and UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Parametrization&lt;/h3&gt;
&lt;p&gt;If you find yourself creating copies of the same Scripted Decision Node but with different starting values (e.g. gathered from state, ESVs, set in constants), you should probably consider building a Custom Node.&lt;/p&gt;
&lt;p&gt;Custom Nodes support &lt;strong&gt;parametrization&lt;/strong&gt; of inputs, including &lt;strong&gt;ESV support&lt;/strong&gt;, meaning that you can build a genericized structure that supports dynamic data retrieved from ESVs, state values, and/or user input to determine its outcomes. Why create a scripted node that returns an access code from one service, for example, when you can create a custom node that returns an access code from &lt;em&gt;any&lt;/em&gt; service given the provided client credentials and secret?&lt;/p&gt;
&lt;h3&gt;Atomic Actions&lt;/h3&gt;
&lt;p&gt;Custom Nodes (and Journey Nodes in general) are meant to be atomic actions. This means that an individual node should do one thing and do that one thing well. As an example, take a look at the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/platform-username.html&quot;&gt;Platform Username Node&lt;/a&gt; — it collects a username and puts that value into Shared State. For a more complex version, there’s the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/attribute-collector.html&quot;&gt;Attribute Collector&lt;/a&gt; — it collects attributes found in the IDM schema and stores those in objectAttributes.&lt;/p&gt;
&lt;p&gt;If you find yourself making a Scripted Decision Node or Custom Node that performs a wide range of diverse actions, consider breaking it down into smaller Custom Nodes. You’ll likely find that each component you’ve designed has its own utility outside of the larger node and, while more nodes are on your canvas, your Journey may become easier to follow since each action is explicitly shown on-screen.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Custom Nodes provide you the following &lt;strong&gt;benefits&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cross-Realm availability (primarily useful for PingAM)&lt;/li&gt;
&lt;li&gt;Display of all usage of the node within the UI and prevention of deletion while in use&lt;/li&gt;
&lt;li&gt;Node availability within the Journey Canvas Node Selector&lt;/li&gt;
&lt;li&gt;Preset, static outcomes&lt;/li&gt;
&lt;li&gt;Built-in import and export utilities&lt;/li&gt;
&lt;li&gt;Parametrization, including passing of ESVs&lt;/li&gt;
&lt;li&gt;Atomic, generic, templated patterning&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When Not to Use a Custom Node&lt;/h2&gt;
&lt;h3&gt;Hyper-Specificity&lt;/h3&gt;
&lt;p&gt;If your created node performs an extremely specific, likely one-off, action it is probably best to keep it a Scripted Decision Node. Creating many one-off nodes has the unintended consequence of cluttering your Custom Nodes viewer and your Node selection list within your Journey canvas and may also result in designers unfamiliar with your node using it in unexpected (likely incorrect) ways in their Journeys.&lt;/p&gt;
&lt;h3&gt;Hyper-Generalization&lt;/h3&gt;
&lt;p&gt;There’s a reason we want our nodes to be self-contained with well-defined boundaries of use: it means that our users who will be building with these nodes will understand exactly what the node does and how to manipulate the inputs in that node to perform the actions they want to take. A super-duper do-it-all node will likely become too complicated to set up (or maintain!) for anyone other than the designer of the node.&lt;/p&gt;
&lt;h3&gt;Libraries&lt;/h3&gt;
&lt;p&gt;Since Nodes are meant to be atomic, their functionality must be self-contained to what is defined in the Node itself. As such, Custom Nodes do not have access to Library Scripts within their scripts.&lt;/p&gt;
&lt;p&gt;If you are looking to create a Custom Node whose functionality would normally leverage a Library Script, see if you can place the necessary functions within the Node’s script editor without bloating the scope of that node too much (see &lt;a href=&quot;#hyper-generalization&quot;&gt;Hyper-Generalization&lt;/a&gt;). Otherwise, a Scripted Decision Node may be more effective in this use case.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Custom Nodes come with the potential &lt;strong&gt;watch-out-fors&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explosion of one-off nodes within the Journey Canvas&lt;/li&gt;
&lt;li&gt;Creation of overly vague “super nodes” that are difficult to use and maintain&lt;/li&gt;
&lt;li&gt;Lack of Library Script support&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Structure a Custom Node&lt;/h2&gt;
&lt;p&gt;As more and more Custom Nodes are designed, it’s important to adopt a common design practice to ensure that maintainers can easily learn from, adopt, modify, and improve each other’s designs. The provided structure was designed with portability, clarity, and consistency in mind for Custom Nodes as well as Scripted Decision Nodes.&lt;/p&gt;
&lt;p&gt;Below is an example structure for Custom Nodes, including comments detailing what each section does.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Title: &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Provide the name of the node here. Makes searching for the node easier in Scripts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Description:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Explain what your node does. How does it work, what does it interact with?&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Make this multi-line as needed.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; *  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Outcomes:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - Success   Description of this outcome&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - Error     Description of this outcome&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Author(s):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - Author Name&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// CONSTANTS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Use this section to define constant values that will be used throughout your script.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * These values should not change during execution of the script.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Placing constants at the top of the script makes it easy to find and modify them if needed.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; CONSTANTS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ERROR_TAG: &lt;/span&gt;&lt;span&gt;&quot;Example Node Name&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    SUCCESS: &lt;/span&gt;&lt;span&gt;&quot;Success&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ERROR: &lt;/span&gt;&lt;span&gt;&quot;Error&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// UTILITIES&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Use this section to define utility functions that will be used throughout your script.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * These functions should be generic and reusable, helping to keep the main logic of your script clean and focused.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * They can include tasks like data manipulation, callbacks definitions, APIs, etc.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Ideally, define these functions in a way that they can be reused in other scripts and nodes as well.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// MAIN&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // Initial (default) outcome. This may be updated based on logic in the try/catch block.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    outcome &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SUCCESS&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         * Main logic of your script goes here.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         * Ensure to handle any potential errors gracefully and update the outcome accordingly.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         * Minimize the amount of code in this section by leveraging utilities defined above.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// PARAMETERS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        /**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         * Get parameters defined for this node&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         * Default values should be set in the node configuration and in the CONSTANTS section above.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;         */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Example parameters&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; exampleBoolean &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; !!&lt;/span&gt;&lt;span&gt;properties.exampleBoolean;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; exampleString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.exampleString &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; properties.exampleString &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;defaultValue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; exampleNumber &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.exampleNumber &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; Number&lt;/span&gt;&lt;span&gt;(properties.exampleNumber) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 42&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; exampleArray &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.exampleArray &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; properties.exampleArray.&lt;/span&gt;&lt;span&gt;toArray&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; exampleObject &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.exampleObject &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; properties.exampleObject &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// LOGIC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// ERROR HANDLING&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ERROR_TAG&lt;/span&gt;&lt;span&gt;} ERROR: ${&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        outcome &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ERROR&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    action.&lt;/span&gt;&lt;span&gt;goTo&lt;/span&gt;&lt;span&gt;(outcome);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A template not including the comments and examples is found below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Title: &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Description:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; *  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Outcomes:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - Success   The node completed successfully&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - Error     The node encountered an error. Check the logs for more details.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Author(s):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * - &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// CONSTANTS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; CONSTANTS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ERROR_TAG: &lt;/span&gt;&lt;span&gt;&quot;Example Node Name ERROR&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    SUCCESS: &lt;/span&gt;&lt;span&gt;&quot;Success&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ERROR: &lt;/span&gt;&lt;span&gt;&quot;Error&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// UTILITIES&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//// MAIN&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    outcome &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SUCCESS&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try&lt;/span&gt;&lt;span&gt; {      &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// PARAMETERS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// LOGIC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        //// ERROR HANDLING&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`${&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ERROR_TAG&lt;/span&gt;&lt;span&gt;}: ${&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        outcome &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; NODE_OUTCOME&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ERROR&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    action.&lt;/span&gt;&lt;span&gt;goTo&lt;/span&gt;&lt;span&gt;(outcome);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful Functionality&lt;/h2&gt;
&lt;h3&gt;Retrieving, Concatenating, and Traversing Values from State&lt;/h3&gt;
&lt;h4&gt;Use Case&lt;/h4&gt;
&lt;p&gt;Very commonly we interact with data retrieved from Node State. In many cases, we may need to retrieve nested data stored in an object and/or array, and may want to concatenate that data with other static (or dynamically-retrieved) data.&lt;/p&gt;
&lt;p&gt;The utility function &lt;code&gt;resolveHandlebars(element, replaceUnresolvedHandlebarsWithString)&lt;/code&gt; provides you a way to do that. It supports nested objects and arrays using &lt;a href=&quot;https://rapidjson.org/md_doc_pointer.html&quot;&gt;JSON Pointer Notation&lt;/a&gt; as well as string interpolation using handlebars &lt;code&gt;{{}}&lt;/code&gt; syntax.&lt;/p&gt;
&lt;p&gt;To use the function within your Custom Node:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Paste the below &lt;a href=&quot;#utility-functions&quot;&gt;Utility Functions&lt;/a&gt; into your node.&lt;/li&gt;
&lt;li&gt;Reference the &lt;code&gt;resolveHandlebars&lt;/code&gt; function against one of your parameters. I like to also set &lt;code&gt;replaceUnresolvedHandlebarsWithString&lt;/code&gt; as a parameter so that the user can decide if the node should do something different if any of the state values are missing.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// Within your MAIN, in the PARAMETERS section&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; replaceUnresolvedHandlebarsWithEmptyString &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; !!&lt;/span&gt;&lt;span&gt;properties.replaceUnresolvedHandlebarsWithEmptyString;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; message &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.message &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; resolveHandlebars&lt;/span&gt;&lt;span&gt;(properties.message, replaceUnresolvedHandlebarsWithEmptyString) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// use whatever property here&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Examples&lt;/h4&gt;
&lt;h5&gt;Simple State Values&lt;/h5&gt;
&lt;p&gt;Use Handlebars to reference state keys directly. This will return the value as stored in state.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{{errorMessage}}&lt;/code&gt; — retrieves the value from &lt;code&gt;nodeState.get(&quot;errorMessage&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{username}}&lt;/code&gt; — retrieves the value from &lt;code&gt;nodeState.get(&quot;username&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{objectAttributes}}&lt;/code&gt; — retrieves the value (&lt;strong&gt;object&lt;/strong&gt;) from &lt;code&gt;nodeState.get(&quot;objectAttributes&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Nested Objects with JSON Pointer Notation&lt;/h5&gt;
&lt;p&gt;For nested objects and arrays, use JSON Pointer Notation within the handlebars. JSON Pointer starts the value with a forward slash (&lt;code&gt;/&lt;/code&gt;) and uses that slash as a delimiter to traverse objects (using the keys) or arrays (using the indices).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{{/objectAttributes/mail}}&lt;/code&gt; — retrieves &lt;code&gt;mail&lt;/code&gt; from the &lt;code&gt;objectAttributes&lt;/code&gt; object&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{/results/0/name}}&lt;/code&gt; — retrieves the &lt;code&gt;name&lt;/code&gt; from the first element of the &lt;code&gt;results&lt;/code&gt; array&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You may be wondering why period or bracket notation isn’t used here. There are three main reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Existing nodes, such as the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/query-filter-decision.html&quot;&gt;Query Filter Decision Node&lt;/a&gt;, as well as IDM use this approach natively.&lt;/li&gt;
&lt;li&gt;Unlike period notation, JSON Pointer supports Objects and Arrays and requires less characters than bracket notation (supporting readability and easier debugging).&lt;/li&gt;
&lt;li&gt;Some Node State keys use periods &lt;em&gt;inside their key&lt;/em&gt; (I’m looking at you, &lt;code&gt;PingOneProtectEvaluationNode.RISK&lt;/code&gt; — &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/pingone/pingone-protect-evaluation.html&quot;&gt;Protect Evaluation Node&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;String Interpolation&lt;/h5&gt;
&lt;p&gt;Combine state values with other values and/or plaintext.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Welcome {{/objectAttributes/givenName}} {{/objectAttributes/sn}}, your risk score is {{/risk/0/score}}&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Utility Functions&lt;/h4&gt;
&lt;p&gt;Add the following constants into the &lt;code&gt;CONSTANTS&lt;/code&gt; section of your Custom Node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; CONSTANTS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    OBJECT_DELIMITER: &lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    HANDLEBAR_REGEX:&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span&gt;\{\{&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*?&lt;/span&gt;&lt;span&gt;)}}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    HANDLEBAR_OPENER: &lt;/span&gt;&lt;span&gt;&quot;{{&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    HANDLEBAR_CLOSER: &lt;/span&gt;&lt;span&gt;&quot;}}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this code into the &lt;code&gt;UTILITIES&lt;/code&gt; section of your Custom Node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Traverse a nested object path within a base object&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@param&lt;/span&gt;&lt;span&gt; {object}&lt;/span&gt;&lt;span&gt; baseObject&lt;/span&gt;&lt;span&gt; The base object to traverse&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@param&lt;/span&gt;&lt;span&gt; {Array&amp;lt;string&amp;gt;}&lt;/span&gt;&lt;span&gt; pathArray&lt;/span&gt;&lt;span&gt; An array of strings representing the path to traverse&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@returns&lt;/span&gt;&lt;span&gt; {any}&lt;/span&gt;&lt;span&gt; The value at the end of the path, or null if not found&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; getNestedValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;baseObject&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;pathArray&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (pathArray.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; ===&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; baseObject;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Check if the baseObject is a valid object and if not try to parse it as JSON&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; baseObject &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &quot;object&quot;&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; baseObject &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; baseObject &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                baseObject &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(baseObject);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                return&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; currentKey &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; pathArray[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (baseObject &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; baseObject[currentKey] &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            return&lt;/span&gt;&lt;span&gt; getNestedValue&lt;/span&gt;&lt;span&gt;(baseObject[currentKey], pathArray.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            return&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Stringifies the element only if necessary&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@param&lt;/span&gt;&lt;span&gt; {any}&lt;/span&gt;&lt;span&gt; element&lt;/span&gt;&lt;span&gt; The element to stringify&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@returns&lt;/span&gt;&lt;span&gt; {string}&lt;/span&gt;&lt;span&gt; The stringified element&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; stringifyIfNeeded&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; type &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; typeof&lt;/span&gt;&lt;span&gt; element;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (type &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;string&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Already a string, return as is&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; element;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; if&lt;/span&gt;&lt;span&gt; (type &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;undefined&apos;&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; type &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;function&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // Return null or an empty string as a consistent fallback for undefined or functions&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // For everything else (objects, arrays, numbers, booleans, null), stringify.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(element);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * Given an element containing handlebar values, retrieve those values from state and replace them in the element.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * If the entire element is a single handlebars value, return the value as-is without stringifying.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * If the value contains JSON pointer notation, traverse the nested object to retrieve the value.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * If the value cannot be found in state, either replace with an empty string or throw an error based on the flag.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@param&lt;/span&gt;&lt;span&gt; {string}&lt;/span&gt;&lt;span&gt; element&lt;/span&gt;&lt;span&gt; The element containing handlebars. This will always be a string&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@param&lt;/span&gt;&lt;span&gt; {boolean}&lt;/span&gt;&lt;span&gt; replaceUnresolvedHandlebarsWithEmptyString&lt;/span&gt;&lt;span&gt; Flag to replace unresolved handlebars with an empty string. If false, an error will be thrown for unresolved handlebars.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;@returns&lt;/span&gt;&lt;span&gt; {any}&lt;/span&gt;&lt;span&gt; The resolved element.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; resolveHandlebars&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;replaceUnresolvedHandlebarsWithEmptyString&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (element) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; newElement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; element;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        var&lt;/span&gt;&lt;span&gt; matches &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; element.&lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HANDLEBAR_REGEX&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (matches &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; matches.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Check if the entire element is a single handlebars value&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            var&lt;/span&gt;&lt;span&gt; isSingleHandlebar &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; matches.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; ===&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; element &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; matches[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            matches.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // Extract the key inside the handlebars&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                var&lt;/span&gt;&lt;span&gt; handlebarsKey &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; match.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HANDLEBAR_OPENER&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HANDLEBAR_CLOSER&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // First, check if the element exists as-is in state&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // This accounts for non-nested values with dots inside the key like &quot;PingOneProtectEvaluationNode.RISK&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                var&lt;/span&gt;&lt;span&gt; resolvedValue &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; nodeState.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(handlebarsKey);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                // If not, check if it&apos;s a nested object using JSON pointer notation&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if&lt;/span&gt;&lt;span&gt; ((resolvedValue &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; resolvedValue &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; handlebarsKey.&lt;/span&gt;&lt;span&gt;indexOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OBJECT_DELIMITER&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    var&lt;/span&gt;&lt;span&gt; stateValuePath &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; handlebarsKey.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CONSTANTS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OBJECT_DELIMITER&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    if&lt;/span&gt;&lt;span&gt; (stateValuePath.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        var&lt;/span&gt;&lt;span&gt; baseObject &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; nodeState.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(stateValuePath[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        if&lt;/span&gt;&lt;span&gt; (stateValuePath.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            // Traverse the nested object path&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            resolvedValue &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; getNestedValue&lt;/span&gt;&lt;span&gt;(baseObject, stateValuePath.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            resolvedValue &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; baseObject;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if&lt;/span&gt;&lt;span&gt; (resolvedValue &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; resolvedValue &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    if&lt;/span&gt;&lt;span&gt; (isSingleHandlebar) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        // If the entire element is handlebars, return the value as-is without stringifying&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        newElement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; resolvedValue;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        newElement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; newElement.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(match, &lt;/span&gt;&lt;span&gt;stringifyIfNeeded&lt;/span&gt;&lt;span&gt;(resolvedValue));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; if&lt;/span&gt;&lt;span&gt; (replaceUnresolvedHandlebarsWithEmptyString) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    newElement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; newElement.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(match, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    throw&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Unable to resolve value for key: ${&lt;/span&gt;&lt;span&gt;handlebarsKey&lt;/span&gt;&lt;span&gt;} within element: ${&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; newElement;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        throw&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Element is null or undefined&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Storing Results and Errors in State&lt;/h3&gt;
&lt;h4&gt;Use Case&lt;/h4&gt;
&lt;p&gt;It’s incredibly common for a node to return a result of an operation into state — either a successful response of data (e.g. the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/pingone/pingone-verify-completion-decision.html&quot;&gt;PingOne Verify Completion Decision Node&lt;/a&gt; returning the transaction in &lt;code&gt;pingOneVerifyTransactionId&lt;/code&gt;) or a failure response with reasoning (e.g. the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/pingone/pingone-verify-evaluation.html&quot;&gt;PingOne Verify Evaluation Node&lt;/a&gt; capturing failures in &lt;code&gt;pingOneVerifyEvaluationFailureReason&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The great part about a Custom Node is that we not only can perform the same utility but we can enable the user to &lt;strong&gt;specify&lt;/strong&gt; where that data should be stored. That way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If this node is being used in multiple places for different reasons, we can store and reference each result separately without fear of overriding an existing value.&lt;/li&gt;
&lt;li&gt;We enable users to standardize where all of their nodes return values (for example, specifying that all error messages get stored in the &lt;code&gt;errorMessage&lt;/code&gt; Shared State Key for easy referencing later).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To enable storage of results and errors in state within your Custom Node:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Paste the below &lt;a href=&quot;#utility-functions-1&quot;&gt;Utility Functions&lt;/a&gt; into your node.&lt;/li&gt;
&lt;li&gt;Add the following two properties to the end of your property list (I like to keep them at the end since they aren’t directly tied to functionality and are placed in a consistent way for users to find):&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;resultSharedStateKey&lt;/td&gt;
&lt;td&gt;Result Shared State Key&lt;/td&gt;
&lt;td&gt;The location in which the result will be stored in Shared State.&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;errorMessageStateKey&lt;/td&gt;
&lt;td&gt;Error Message State Key&lt;/td&gt;
&lt;td&gt;If an error occurs, the location in Shared State where the error message will be stored.&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;errorMessage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you &lt;em&gt;always&lt;/em&gt; want a result or error message to be pushed to state, enforce the parameter to be &lt;code&gt;required&lt;/code&gt; from within the parameter configuration.&lt;/p&gt;
&lt;h4&gt;Examples&lt;/h4&gt;
&lt;p&gt;Let’s say you have a Configuration Provider that displays a message containing the results of a Custom Node’s evaluation.&lt;/p&gt;
&lt;p&gt;You could set up your Custom Node to push the results &lt;strong&gt;and&lt;/strong&gt; error messages to &lt;code&gt;output&lt;/code&gt;. Then, the config provider would display the resulting information regardless of whether it’s a result or failure without any additional logic.&lt;/p&gt;
&lt;p&gt;You could set up your Custom Node to push the results to &lt;code&gt;myResults&lt;/code&gt; and error message to &lt;code&gt;myErrors&lt;/code&gt;. That way, the config provider could display results or errors with different text depending on what it receives.&lt;/p&gt;
&lt;p&gt;You could set up your Custom Node to push results to &lt;code&gt;myResults&lt;/code&gt; and leave error messages empty — resulting in only results and no error messages being passed to the config provider. The same could be said the other direction — you could leave results empty and then the config provider could only get error messages.&lt;/p&gt;
&lt;h4&gt;Utility Functions&lt;/h4&gt;
&lt;p&gt;Paste this within the &lt;code&gt;PARAMETERS&lt;/code&gt; section of your &lt;code&gt;MAIN&lt;/code&gt; within your Custom Node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; resultSharedStateKey &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.resultSharedStateKey &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; properties.resultSharedStateKey &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; errorMessageStateKey &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; properties.errorMessageStateKey &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; properties.errorMessageStateKey &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After your node has performed its action successfully, add this line to your &lt;code&gt;MAIN&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// change `result` to the result you want to store&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!!&lt;/span&gt;&lt;span&gt;resultSharedStateKey) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    nodeState.&lt;/span&gt;&lt;span&gt;putShared&lt;/span&gt;&lt;span&gt;(resultSharedStateKey, result);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the &lt;code&gt;ERROR HANDLING&lt;/code&gt; section of your &lt;code&gt;MAIN&lt;/code&gt; within your Custom Node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(e) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    //// ERROR HANDLING&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    logger.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Error during object creation: ${&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!!&lt;/span&gt;&lt;span&gt;errorMessageStateKey) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        nodeState.&lt;/span&gt;&lt;span&gt;putShared&lt;/span&gt;&lt;span&gt;(errorMessageStateKey, e);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    outcome &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; NodeOutcome.&lt;/span&gt;&lt;span&gt;ERROR&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;p&gt;Below are some common gotchas encountered when working with Custom Nodes.&lt;/p&gt;
&lt;h3&gt;Property Gotchas&lt;/h3&gt;
&lt;h4&gt;Multi-Value Properties&lt;/h4&gt;
&lt;p&gt;If your property is multivalued, you’ll need to convert that property to an array before using it in your function (e.g. &lt;code&gt;properties.exampleArray.toArray()&lt;/code&gt;)&lt;/p&gt;
&lt;h4&gt;Enumerated Properties&lt;/h4&gt;
&lt;p&gt;Enumerated Values are defined in the Node property configuration as the &lt;strong&gt;Key&lt;/strong&gt; being the &lt;em&gt;value&lt;/em&gt; that returns to the script and the &lt;strong&gt;Value&lt;/strong&gt; as the &lt;em&gt;label&lt;/em&gt; that is used in the selector within the node.&lt;/p&gt;
&lt;p&gt;Additionally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Key has restrictions on what characters it can support — consider using &lt;code&gt;CONSTANT_CASE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Value supports most characters — consider using human-readable, &lt;code&gt;Title Case&lt;/code&gt; with spaces.&lt;/li&gt;
&lt;li&gt;The Default Value matches the Keys in the Enumerated Values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As an example, if you wanted to create a dropdown list in which a user selected the “Message Type” when displaying a message to the user, your property configuration might look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;messageType&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Message Type&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;The type of Message to display to the user.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;STRING&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;required&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;defaultValue&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;INFORMATION&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;options&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;INFORMATION&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Information&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;ERROR&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;WARNING&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Warning&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;multivalued&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this setup, the user will see and be able to select the options “Information”, “Error”, and “Warning”.&lt;/p&gt;
&lt;p&gt;When selected, the value “INFORMATION”, “ERROR”, or “WARNING” is passed into the script.&lt;/p&gt;
&lt;p&gt;The default value is “INFORMATION”, meaning that the user will see “Information” by default and if unchanged the value “INFORMATION” will be passed into the script.&lt;/p&gt;
&lt;p&gt;It is to your advantage to define the dropdown values as a dictionary (JSON object) in your script in which your passed value is mapped to a static value or state.&lt;/p&gt;
&lt;h3&gt;Outcome Gotchas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Outcomes are static and case-sensitive. If your code returns an outcome of &lt;code&gt;Success&lt;/code&gt; that outcome should be defined as &lt;code&gt;Success&lt;/code&gt; (&lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;SUCCESS&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;On a similar topic, you should make your outcomes &lt;code&gt;Title Case&lt;/code&gt; with spaces to match the outcomes in a standard Journey Node. It’ll make your node look and feel like the rest on the canvas.&lt;/li&gt;
&lt;li&gt;You can configure a Custom Node to return an error outcome automatically. If you do, the outcome is set as &lt;code&gt;Script Error&lt;/code&gt;. Any error outcomes (&lt;code&gt;Error&lt;/code&gt;, &lt;code&gt;Script Error&lt;/code&gt;, etc) defined in code must still be defined in the configuration separately as the &lt;code&gt;Script Error&lt;/code&gt; outcome is only returned on an error that isn’t handled. With this in mind, I suggest choosing either the built-in error or defining your own: not using both.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node Management Gotchas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;If you attempt to import a Custom Node with the same &lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;code&gt;id&lt;/code&gt;&lt;/strong&gt; into your tenant the import will fail. To fix, you must change the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; within the JSON export prior to importing.&lt;/li&gt;
&lt;li&gt;If you change a Custom Node, the new version of that custom node will be applied to all Journeys using it &lt;strong&gt;immediately&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Descriptions support the same basic HTML formatting as the content in &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/page.html&quot;&gt;Page Nodes&lt;/a&gt;. Use that functionality to add greater detail like &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; blocks, links, and paragraph/list formatting. Markdown is not supported.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gotchas as of This Article&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Object Parameter types do not support HTML formatting in their descriptions.&lt;/li&gt;
&lt;li&gt;Custom Nodes do not support versioning. Consider storing exports in an external git repository.&lt;/li&gt;
&lt;li&gt;The UI forces Multi-Valued and Enumerated values to be &lt;code&gt;required&lt;/code&gt; even though they technically don’t need to be. To get around this, use the API to update the node config with &lt;code&gt;required&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt; and the UI will work as normal.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Through this guide we have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learned when and when not to use Custom Nodes&lt;/li&gt;
&lt;li&gt;Worked through a standard structure to designing Custom Nodes and Scripted Decision Nodes&lt;/li&gt;
&lt;li&gt;Learned of some common utility functions to kickstart Custom Node development&lt;/li&gt;
&lt;li&gt;Walked through common “gotchas” when working with Custom Nodes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Custom Nodes are an incredibly powerful tool that massively improves collaborative Journey development. Knowing when and how to leverage them for your team will make you all the more effective in your IAM journey.&lt;/p&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.Vuy-pb8f_1nE3xJ.webp" medium="image" type="image/webp" width="2752" height="1536"/></item><item><title>Introducing macOS Platform SSO in PingID Desktop 2.0</title><link>https://developer.pingidentity.com/blog/introducing-macos-platform-sso-in-pingid-desktop-2-0/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/introducing-macos-platform-sso-in-pingid-desktop-2-0/</guid><description>A technical overview of how macOS Platform SSO in PingID Desktop 2.0 connects the native Mac sign-on experience to PingFederate for seamless browser and native app SSO.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;macOS Platform SSO in PingID Desktop 2.0 gives developers and identity architects a way to connect the native Mac sign-on experience to PingFederate and extend that trust into browser-based applications and native apps that rely on system web authentication. This isn’t just a desktop UI update. It’s an end-to-end flow built from an MDM-delivered Platform SSO profile, PingID Desktop 2.0, PingOne for Workforce (PingID), and PingFederate 13.1 or later.&lt;/p&gt;
&lt;p&gt;In this first release, PingFederate is the supported identity provider (IdP), the Platform SSO extension is installed as part of PingID Desktop 2.0, and the supported authentication method is Secure Enclave-backed keys.&lt;/p&gt;
&lt;p&gt;This post focuses on the implementation side of the release: prerequisites, architecture, PingFederate configuration, MDM profile requirements, and the main flow boundaries you should understand before rollout.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before you start, make sure the deployment baseline is in place.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;macOS 15.4 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;Apple Silicon Mac&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device management&lt;/td&gt;
&lt;td&gt;MDM is required, and unattended Automatic Device Enrollment (ADE) is supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop client&lt;/td&gt;
&lt;td&gt;PingID Desktop 2.0 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ping service&lt;/td&gt;
&lt;td&gt;PingOne for Workforce (PingID) is required, and users must exist there before registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity provider&lt;/td&gt;
&lt;td&gt;PingFederate 13.1 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingFederate component&lt;/td&gt;
&lt;td&gt;Mac Platform SSO adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you are already using PingID Desktop 1.0, the client-side change is straightforward: upgrade to PingID Desktop 2.0. No additional configuration is required in the installer or in the running application itself to enable macOS Platform SSO. The configuration is delivered through MDM and the PingFederate-side setup.&lt;/p&gt;
&lt;h2&gt;Why Platform SSO matters&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://support.apple.com/guide/deployment/platform-sso-for-macos-dep7bbb05313/web&quot;&gt;Platform SSO&lt;/a&gt; is Apple’s framework for binding the local macOS sign-on experience to an enterprise identity provider. In Ping’s implementation, the model combines device identity, user identity, cryptographic trust, token issuance, and application single sign-on (SSO) in a single flow.&lt;/p&gt;
&lt;p&gt;Platform SSO is also different from browser-session-based SSO. It doesn’t rely on an existing browser cookie to create the SSO experience. Because trust is established through the platform and the identity provider integration, even a newly installed supporting browser (based on Apple’s WebKit) can participate in SSO right away. The same model can also benefit native applications that use the same PingFederate login domain through supported system web authentication flows.&lt;/p&gt;
&lt;p&gt;From a technical point of view, that matters because the trust does not begin in the browser. The device is registered first, the user is registered second, and only then does the system use that registered state to satisfy later authorization requests. That is what makes the experience feel native while still keeping PingFederate in control of policy and token issuance.&lt;/p&gt;
&lt;h2&gt;What we are releasing&lt;/h2&gt;
&lt;p&gt;This release adds macOS Platform SSO support to the new PingID Desktop application, starting with version 2.0. The Platform SSO extension is installed as part of PingID Desktop and works with PingFederate as the supported identity provider for this release.&lt;/p&gt;
&lt;p&gt;At a high level, the solution has three major phases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Device registration&lt;/li&gt;
&lt;li&gt;User registration&lt;/li&gt;
&lt;li&gt;Login and application authorization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those phases matter because Platform SSO is not just a browser shortcut. The flow establishes trust in the device, binds a user credential to that device, and then uses that trust to support later sign-on requests.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;The architecture includes four collaborating parts: the MDM profile that delivers the required configuration, the macOS Platform SSO engine, the PingID SSO extension, and PingFederate as the identity provider runtime layer.&lt;/p&gt;
&lt;p&gt;The MDM profile is what turns the feature on for the device and tells macOS where to send matching authentication traffic. At the profile level, developers should expect a &lt;code&gt;com.apple.extensiblesso&lt;/code&gt; payload with values such as the Ping extension identifier, the Ping team identifier, the list of URLs that should be handled by the extension, and ExtensionData that carries the Platform SSO configuration. Inside that configuration, the important values are the client identifier, base URL, audience, and PingFederate instance identifier that the extension uses when talking to PingFederate. For this first release, the Platform SSO authentication method is &lt;code&gt;UserSecureEnclaveKey&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The macOS Platform SSO engine handles platform-native behavior, such as key generation, authentication, token refresh, and secure token storage. The PingID SSO extension handles device registration, user registration, token and login configuration, and the interception of supported SSO requests. PingFederate receives the Platform SSO-augmented requests, validates tokens, Secure Enclave signatures, nonce and replay protections, and device binding, then issues the client-specific result for the application or browser flow.&lt;/p&gt;
&lt;p&gt;From an implementer’s point of view, the flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The device is enrolled and receives the required Platform SSO profile through MDM.&lt;/li&gt;
&lt;li&gt;The device completes registration and establishes a cryptographic device identity.&lt;/li&gt;
&lt;li&gt;The user completes registration and binds a user credential to that device.&lt;/li&gt;
&lt;li&gt;When a supported authorization request arrives, macOS routes it to the Platform SSO extension.&lt;/li&gt;
&lt;li&gt;The extension adds the required Platform SSO material and forwards the request to PingFederate.&lt;/li&gt;
&lt;li&gt;PingFederate validates the request and continues the authentication policy.&lt;/li&gt;
&lt;li&gt;The application receives the result of the PingFederate flow without forcing the user to manually repeat sign-on.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;What you need to configure&lt;/h2&gt;
&lt;p&gt;To roll out macOS Platform SSO, there are three technical control planes to configure: device management, PingOne for Workforce, and PingFederate. You can find full details in &lt;a href=&quot;https://docs.pingidentity.com/pingone/strong_authentication_mfa/p1_pid_psso_macos_config.html&quot;&gt;Configuring Platform SSO for macOS (Workforce only)&lt;/a&gt; in the PingOne documentation.&lt;/p&gt;
&lt;h3&gt;1. Device management and MDM profile&lt;/h3&gt;
&lt;p&gt;The device must be Apple Silicon, run macOS 15.4 or later, and be managed by MDM so it can receive the Platform SSO configuration profile. Unattended ADE is also supported, which is useful for zero-touch provisioning of managed Mac fleets.&lt;/p&gt;
&lt;p&gt;The MDM profile is where you define the full Platform SSO behavior for the device, not just the redirect target. The exact values depend on your environment, but the key structure should look like this (keys can also change according to needs):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;Type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Redirect&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;URLs&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;https://&amp;lt;pingfederate-host&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;PlatformSSO&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;Account&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;DisplayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;My Org Name&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;UserCreation&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;EnableAtLogin&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;TokenToUserMapping&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;FullName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;AccountName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;preferred_username&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;EnableFirstUserDuringSetup&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;EnableRegistrationDuringSetup&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;RegistrationToken&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&amp;lt;registration-token taken from the PingFederate Adapter&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;UseSharedDeviceKeys&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;AuthenticationMethod&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;UserSecureEnclaveKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;AllowDeviceIdentifiersInAttestation&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;ExtensionData&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;AppPolicy&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;AppAllowList&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;&amp;lt;bundle-id-1&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;&amp;lt;bundle-id-2&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;AppBlockList&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;&amp;lt;blocked-bundle-id-1&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;&amp;lt;blocked-bundle-id-2&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;AllowManagedAppsOnly&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;PSSOConfiguration&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;BaseURL&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://&amp;lt;pingfederate-host&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;Audience&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://&amp;lt;pingfederate-host&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;ClientID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&amp;lt;oauth-client-id&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;InstanceID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&amp;lt;pingfederate-instance-id&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;ExtensionComposedIdentifier&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;com.pingidentity.pingid.desktop.pingssoe (6U3RF4C84N)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ExtensionComposedIdentifier&lt;/code&gt; (in Declarative Device Management as in the example above) or &lt;code&gt;ExtensionIdentifier&lt;/code&gt; and &lt;code&gt;TeamIdentifier&lt;/code&gt; (in a regular configuration profile) will always hold &lt;code&gt;com.pingidentity.pingid.desktop.pingssoe&lt;/code&gt; and &lt;code&gt;6U3RF4C84N&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;Platform SSO deployments also need a separate associated domains payload. This payload authorizes Safari and system authentication services to route authentication traffic to the PingID Desktop SSO extension. Each entry should use the &lt;code&gt;authsrv:&lt;/code&gt; prefix followed by the host name only. Do not include &lt;code&gt;https://&lt;/code&gt;, a port, or a trailing slash. For example, if your PingFederate base URL is &lt;code&gt;https://pf.example.com&lt;/code&gt;, the associated domain value should be &lt;code&gt;authsrv:pf.example.com&lt;/code&gt;. Add one entry for each domain your deployment uses. &lt;code&gt;ApplicationIdentifier&lt;/code&gt; will always be &lt;code&gt;6U3RF4C84N.com.pingidentity.pingid.desktop&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A placeholder associated domains payload can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;PayloadType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;com.apple.developer.associated-domains&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;Configuration&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;ApplicationIdentifier&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;6U3RF4C84N.com.pingidentity.pingid.desktop&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;AssociatedDomains&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;authsrv:&amp;lt;your-pingfederate-host&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few fields are especially important during implementation. The &lt;code&gt;URLs&lt;/code&gt; list and the associated domains payload work together to determine which login traffic the extension can handle. The &lt;code&gt;TokenToUserMapping&lt;/code&gt; section controls how claims from the identity response (OIDC ID Token) map to the local macOS account. The &lt;code&gt;EnableFirstUserDuringSetup&lt;/code&gt; and &lt;code&gt;EnableRegistrationDuringSetup&lt;/code&gt; flags are useful when you want registration to happen during Setup Assistant, especially in unattended ADE deployments. The &lt;code&gt;PSSOConfiguration&lt;/code&gt; block must align with the PingFederate-side client, audience, base URL, and instance configuration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note about EnableFirstUserDuringSetup:&lt;/strong&gt; If you enable this field as part of ADE, macOS shows a native username and password screen that prompts the user for IdP credentials. The system authenticates the user against the IdP using the Password grant type. macOS then carries the response into the next screen, where it creates the local account. In the local account creation screen, the account name and full name fields are pre-populated, and the user only needs to create the local account password. This prevents the possibility of users giving their own account and machine names.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. PingOne for Workforce presence&lt;/h3&gt;
&lt;p&gt;PingOne for Workforce is part of the solution. Users participating in the flow must exist in PingOne for Workforce before registration starts. That presence is what enables device registration, user registration, and strong authentication during the Platform SSO journey.&lt;/p&gt;
&lt;p&gt;The Platform SSO flow also supports PingOne for Workforce as the MFA layer, including methods such as push notifications to the PingID mobile application and passkeys.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note about created users in PingOne for Workforce:&lt;/strong&gt; To support the use of the MDM profile key &lt;code&gt;TokenToUserMapping&lt;/code&gt; - &lt;code&gt;Account Name&lt;/code&gt;, the admin must create a custom User Attribute named &lt;code&gt;preferred-username&lt;/code&gt; in PingOne for Workforce that will map the username to the PingFederate field inserted into the &lt;code&gt;preferred_username&lt;/code&gt; field in the ID Token. Each user must have this Custom Attribute.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Admins can check if the user is registered to the Platform SSO in the &lt;strong&gt;User&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Services&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Authentication&lt;/strong&gt; section under &lt;strong&gt;Sessions &amp;amp; Devices&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;3. PingFederate 13.1+ configuration&lt;/h3&gt;
&lt;p&gt;Customers should use PingFederate 13.1 or later, and in practice the latest supported PingFederate deployment is the best starting point.&lt;/p&gt;
&lt;p&gt;The PingFederate configuration should include the following components and they should be aligned as one end-to-end Platform SSO flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Import the Apple Enterprise Attestation Root CA so PingFederate can validate the attestation chain presented during registration.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;urn:apple:platformsso&lt;/code&gt; scope.&lt;/li&gt;
&lt;li&gt;Configure an OAuth client for the desktop app. The required grant types are Authorization Code, Refresh Token, and Assertion Grant.&lt;/li&gt;
&lt;li&gt;Make sure the client can complete the OIDC flow and issue both an access token and an ID token.&lt;/li&gt;
&lt;li&gt;Create an access token manager for the Platform SSO flow.&lt;/li&gt;
&lt;li&gt;Create or update the OpenID Connect policy used to issue and validate the required claims.&lt;/li&gt;
&lt;li&gt;Create a Mac Platform SSO adapter in PingFederate.&lt;/li&gt;
&lt;li&gt;Create a Mac Platform SSO JWT Bearer Grant Processor.&lt;/li&gt;
&lt;li&gt;Create the IdP connection used by that grant processor.&lt;/li&gt;
&lt;li&gt;Add the Platform SSO adapter to the authentication policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Treat these as connected configuration objects rather than isolated tasks. The OAuth client, tokens, adapter, grant processor, IdP connection, and authentication policy all need to agree on the same Platform SSO flow so registration and sign-on behave consistently.&lt;/p&gt;
&lt;p&gt;A few implementation details are especially important:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Platform SSO must be the first step in the PingFederate authentication policy. If another adapter appears before it, the flow is not supported.&lt;/li&gt;
&lt;li&gt;Customers can use PingFederate HTML Adapters or a DaVinci flow later in the authentication experience. This way, admins can take the advantage of different MFA options (such as number matching push notifications to PingID mobile app or passkeys etc.), PingOne Verify, and so on.&lt;/li&gt;
&lt;li&gt;The OAuth client, access token manager, adapter, and IdP connection should be configured consistently so the same client and token context are used across registration and SSO.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;If registration or sign-on does not behave as expected, collect logs from all three layers of the flow: macOS, PingID Desktop, and PingFederate. Looking at only one layer usually gives you only part of the story.&lt;/p&gt;
&lt;h3&gt;1. Stream macOS unified logs&lt;/h3&gt;
&lt;p&gt;Use macOS unified logging when you want to watch the flow in real time while reproducing the issue.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; stream&lt;/span&gt;&lt;span&gt; --predicate&lt;/span&gt;&lt;span&gt; &apos;process == &quot;AppSSOAgent&quot;&apos;&lt;/span&gt;&lt;span&gt; --info&lt;/span&gt;&lt;span&gt; --debug&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; stream&lt;/span&gt;&lt;span&gt; --predicate&lt;/span&gt;&lt;span&gt; &apos;process == &quot;pingssoe&quot; AND subsystem BEGINSWITH &quot;com.pingid.passwordless&quot;&apos;&lt;/span&gt;&lt;span&gt; --info&lt;/span&gt;&lt;span&gt; --debug&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or for the ADE flow, in addition to the above (Terminal can be opened by using ⌃+⌥+⌘+T):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; stream&lt;/span&gt;&lt;span&gt; --predicate&lt;/span&gt;&lt;span&gt; &apos;process == &quot;Setup Assistant&quot; AND subsystem == &quot;com.apple.macbuddy&quot;&apos;&lt;/span&gt;&lt;span&gt; --info&lt;/span&gt;&lt;span&gt; --debug&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice, the most useful predicates are the ones that narrow the stream to the PingID Desktop app, the SSO extension, or Apple AppSSO activity. If you already know the process name, subsystem, or category you want to follow, add it to the predicate and reproduce the registration or sign-on flow again.&lt;/p&gt;
&lt;h3&gt;2. Collect PingID Desktop logs&lt;/h3&gt;
&lt;p&gt;For PingID Desktop, the fastest way to gather diagnostics is the built-in CLI.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pingiddesktop&lt;/span&gt;&lt;span&gt; logs&lt;/span&gt;&lt;span&gt; --time&lt;/span&gt;&lt;span&gt; 30m&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output path is &lt;code&gt;/tmp/pingid-desktop-logs.zip&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;3. Review PingFederate server logs&lt;/h3&gt;
&lt;p&gt;On the server side, review the PingFederate logs while reproducing the same registration or sign-on attempt. This is especially important when you need to confirm adapter execution, token issuance, attestation validation, policy progression, or failures in the JWT Bearer Grant Processor or IdP connection.&lt;/p&gt;
&lt;p&gt;For Platform SSO issues, it is a good practice to collect PingFederate logs in DEBUG during testing so you can correlate the server-side event with the device-side and desktop-side activity.&lt;/p&gt;
&lt;h2&gt;Current scope&lt;/h2&gt;
&lt;p&gt;This first release is intentionally focused.&lt;/p&gt;
&lt;p&gt;Today, the supported authentication method is Secure Enclave-backed keys. In the current design, Phase 1 support is limited to the Secure Enclave approach, and the configuration examples use &lt;code&gt;UserSecureEnclaveKey&lt;/code&gt; for the authentication method.&lt;/p&gt;
&lt;p&gt;That limitation is important because the registration and assertion model depends on hardware-backed keys and attestation as part of the trust chain.&lt;/p&gt;
&lt;p&gt;It is also important to understand what the extension does and does not intercept. The PingID SSO extension supports browser-based authentication flows and system web authentication sessions. It does not intercept direct API traffic such as cURL, Postman, background service calls, URLSession requests, or JavaScript &lt;code&gt;fetch()&lt;/code&gt; calls, because those do not go through the OS-level browser authentication path.&lt;/p&gt;
&lt;h2&gt;Why this matters for developers and implementers&lt;/h2&gt;
&lt;p&gt;For engineering teams standardizing on managed Macs, this release creates a clean separation of responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MDM delivers the Platform SSO profile.&lt;/li&gt;
&lt;li&gt;PingID Desktop 2.0 hosts the extension.&lt;/li&gt;
&lt;li&gt;PingOne for Workforce provides the user and MFA context.&lt;/li&gt;
&lt;li&gt;PingFederate enforces policy and issues the final authentication result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That split makes the system easier to reason about during rollout and troubleshooting. The device and user are enrolled once, and later authorization requests can be evaluated using signed proofs tied to that registered state instead of forcing the user through repeated sign-ons.&lt;/p&gt;
&lt;p&gt;It also means troubleshooting gets easier when you keep the layers separate. If registration is failing, start with MDM profile delivery, the PingID Desktop logs, and the PingFederate debug logs. If sign-on works in the browser but not through Platform SSO, check whether the request is actually using a supported browser or system web authentication path and whether Platform SSO is the first step in the PingFederate policy.&lt;/p&gt;
&lt;h2&gt;Looking ahead&lt;/h2&gt;
&lt;p&gt;This is the beginning of the Platform SSO journey in PingID Desktop. The current release focuses on PingFederate as the supported identity provider and on Secure Enclave-backed authentication. Support for additional authentication types and broader coverage will follow in future releases.&lt;/p&gt;
&lt;p&gt;If you are planning a rollout now, the best place to start is with PingID Desktop 2.0, the latest PingFederate deployment, the Mac Platform SSO adapter, PingOne for Workforce, and an MDM-managed device fleet. That gives you the baseline you need to validate registration, confirm policy behavior, and test browser and native application sign-on with the supported architecture.&lt;/p&gt;
&lt;p&gt;macOS Platform SSO in PingID Desktop 2.0 is a practical step toward a more native and lower-friction sign-on experience on managed Macs. For developers and identity teams building passwordless and device-aware experiences around PingFederate, this is the right place to start.&lt;/p&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.cl4H7TMu_ZbJg28.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Mapping the landscape for AI-first headless identity</title><link>https://developer.pingidentity.com/blog/mapping-the-landscape-for-headless/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/mapping-the-landscape-for-headless/</guid><description>A short survey to gather developer and admin feedback on headless IAM workflows and AI-assisted identity administration.</description><pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As we continue to enhance &lt;a href=&quot;https://www.pingidentity.com/en/resources/blog/post/headless-identity-accelerates-agentic-enterprise.html&quot;&gt;AI-first headless methods of working with Ping Identity systems&lt;/a&gt;, we want to know more about your experiences and needs. If you’re exploring headless IAM workflows that enable agentic operations or wondering how AI can streamline the administrative experience with Ping’s platform, we’d love your feedback.&lt;/p&gt;
&lt;p&gt;We’re running a short survey to better understand how teams think about using AI agents for IAM admin, where current experiences fall short in terms of headless capabilities and context for agents, and which problems matter most to solve next. Your input will help us prioritize both product direction and follow-up research in this space.&lt;/p&gt;
&lt;p&gt;We’re especially interested in feedback on questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What makes headless admin valuable or challenging in real-world environments?&lt;/li&gt;
&lt;li&gt;What guardrails and contextual guidance would best support agents in safely designing, configuring, and operating your specific identity infrastructure?&lt;/li&gt;
&lt;li&gt;Which admin workflows feel most important to improve first?&lt;/li&gt;
&lt;li&gt;What signals help you trust and adopt new approaches in identity administration?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI-first headless identity isn’t just about removing UI: it’s about giving teams and their AI tools and agents more flexible, intelligent ways to manage identity workflows, reduce manual effort, and contextually adapt administration to the needs of their environment. Your feedback can tell us where that vision is already resonating and where more clarity, capability, or support is needed.&lt;/p&gt;
&lt;p&gt;If you’re willing to go a step further, the survey also includes options for more engagement beyond the survey and the blog.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pingidentity.qualtrics.com/jfe/form/SV_6zD2mzOljaQbrlY&quot;&gt;Take the survey&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you are a PingOne user, you might find a version of the survey in the Research section of Help &amp;amp; Resources in the top nav bar.&lt;/p&gt;
&lt;p&gt;If we get at least 30 complete responses, we’ll share the survey results in a future post.&lt;/p&gt;
&lt;p&gt;Thanks in advance for helping us shape the future of AI-first headless identity admin at Ping Identity.&lt;/p&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.DuKruISx_Z2ofrrw.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>React Native SDK 1.0 Release!</title><link>https://developer.pingidentity.com/blog/react-native-sdk-1-0-release/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/react-native-sdk-1-0-release/</guid><description>Announcing the Ping Identity React Native SDK 1.0 — a TypeScript monorepo giving React Native apps full access to native authentication, token lifecycle, MFA, and Journey orchestration without trade-offs.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;The problem we set out to solve&lt;/h2&gt;
&lt;p&gt;Cross-platform development promises one codebase for both iOS and Android, but authentication is usually where that promise gets complicated. A production app needs more than a login screen. It has to handle token lifecycle, secure storage, biometrics, push, external identity providers, and multi-step orchestration, all from a single codebase.&lt;/p&gt;
&lt;p&gt;The existing options usually forced a trade-off. Thin wrappers left the hard parts to the developer, browser-only flows limited how native the experience could feel, and custom bridges meant maintaining native code separately for both platforms. That’s why we’ve built our Ping Orchestration React Native SDK to close the gap: a TypeScript API that exposes the real capabilities of the underlying native SDKs without abstracting away the complexity you still need access to.&lt;/p&gt;
&lt;h2&gt;What’s in the toolbox&lt;/h2&gt;
&lt;p&gt;The SDK is a Yarn monorepo of independent packages. That means applications can install what they actually need instead of pulling in a single oversized package.&lt;/p&gt;
&lt;p&gt;The SDK supports both Journey orchestration and standard OIDC flows. It handles the flow from the initial challenge through token handling, including redirect handling, PKCE, refresh, and session management. Those pieces are already built in, so the app doesn’t need to wire them up separately.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useJourney&lt;/code&gt; returns a &lt;code&gt;[node, actions]&lt;/code&gt; tuple. &lt;code&gt;node&lt;/code&gt; is a discriminated union for the current Journey state. Loading and error states live on &lt;code&gt;actions&lt;/code&gt;. If the flow needs to be shared across screens, &lt;code&gt;JourneyProvider&lt;/code&gt; can hold that state at the navigator level instead of passing it through props.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useJourneyForm&lt;/code&gt; is an optional hook for normalizing the callback array returned by a Journey step. It maps callbacks into typed fields with &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, and &lt;code&gt;executionMode&lt;/code&gt;, covering regular inputs, background callbacks, and native handoffs like FIDO. That lets the UI decide whether to render an input, run work automatically, or delegate to a native capability. It also exposes &lt;code&gt;canSubmit&lt;/code&gt;, &lt;code&gt;issues&lt;/code&gt;, and a prebuilt &lt;code&gt;form.input&lt;/code&gt; object that can be passed directly to &lt;code&gt;actions.next()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;MFA is part of the same model. Passkeys, TOTP, push MFA, and device binding are implemented as dedicated packages that plug into the flow rather than forcing a separate native project or a second bridge layer.&lt;/p&gt;
&lt;p&gt;Token storage uses the native secure store on each platform. Device signals and device-bound features are separate packages, so apps can add them only if they need them.&lt;/p&gt;
&lt;p&gt;The TypeScript surface is intentionally strict. Nodes, fields, config, and errors are typed unions, so application code can narrow correctly.&lt;/p&gt;
&lt;p&gt;What the SDK covers is only part of the story. The other part is how those capabilities are exposed to application code.&lt;/p&gt;
&lt;h2&gt;How we designed the API&lt;/h2&gt;
&lt;p&gt;A big part of the work was making sure the API stayed predictable from the React Native side, while still exposing the native capabilities underneath.&lt;/p&gt;
&lt;h3&gt;Factory functions, not classes&lt;/h3&gt;
&lt;p&gt;Every client is created with a factory function that returns a plain object. There is no &lt;code&gt;new&lt;/code&gt;, no inheritance, and no &lt;code&gt;this&lt;/code&gt;. Configuration is validated at creation time, and the native instance initializes lazily on the first call. The result composes cleanly into a context, ref, or module-level singleton.&lt;/p&gt;
&lt;h3&gt;A consistent client contract&lt;/h3&gt;
&lt;p&gt;All authentication clients, such as OIDC and Journey, share the same surface: &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;refresh&lt;/code&gt;, &lt;code&gt;revoke&lt;/code&gt;, and &lt;code&gt;dispose&lt;/code&gt;. Return types are discriminated throughout, so TypeScript narrows correctly at every call site.&lt;/p&gt;
&lt;h3&gt;React hooks, standalone or with a provider&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useJourney&lt;/code&gt; and &lt;code&gt;useOidc&lt;/code&gt; return a &lt;code&gt;[state, actions]&lt;/code&gt; tuple. A client can be passed directly, or the argument can be omitted, and the hook will read from the nearest provider.&lt;/p&gt;
&lt;p&gt;A screen can use a client directly when the flow is local:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;actions&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; useJourney&lt;/span&gt;&lt;span&gt;(journeyClient)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or the application can provide the client higher in the tree and let screens read it from context:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// Root component&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;JourneyProvider client&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{journeyClient}&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;App &lt;/span&gt;&lt;span&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;JourneyProvider&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The bridge: New Architecture and Classic, transparently&lt;/h3&gt;
&lt;p&gt;Every package supports both TurboModules and the classic bridge. The TypeScript layer resolves the correct native module at runtime, so application code does not need to handle either case. Moving an app to the New Architecture doesn’t require any changes to how the SDK is called.&lt;/p&gt;
&lt;p&gt;In practice, that means the packages follow the same general patterns, while still allowing the app to work closer to native behavior when needed.&lt;/p&gt;
&lt;h2&gt;What developers can build with the SDK&lt;/h2&gt;
&lt;p&gt;The SDK is not prescriptive about your app’s structure. Because it covers the entire identity surface area, you have the flexibility to build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Passwordless sign-on&lt;/strong&gt;: Implement passkeys and biometrics, with a Journey fallback for seamless enrollment or account recovery.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Software TOTP&lt;/strong&gt;: Enable users to enroll and generate verification codes directly inside the app instead of forcing them to switch to a separate authenticator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push MFA approval flows&lt;/strong&gt;: Design custom approval flows that match the app’s design system rather than dropping users into a generic web view.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device-bound sessions&lt;/strong&gt;: Securely tie user credentials directly to a specific physical device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Social and external IdP handoff&lt;/strong&gt;: Let the SDK manage the browser steps, token exchange, and resulting session states.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background device signal collection&lt;/strong&gt;: Gather silent device signals as part of a Journey flow for real-time risk evaluation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following table maps those capabilities to the documented use cases and usage guides available today:&lt;/p&gt;
&lt;div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case or guide&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/fido/index.html&quot;&gt;FIDO and passkeys&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Passwordless sign-on with passkeys and biometrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/push/index.html&quot;&gt;Push notifications&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Push-based MFA approval flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/oath/index.html&quot;&gt;OATH one-time passcodes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Software TOTP enrollment and verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/external-idp/index.html&quot;&gt;Social sign-on&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;External IdP handoff and token exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/device-binding/index.html&quot;&gt;Device binding&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Binding credentials to a specific physical device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/device-profiling/index.html&quot;&gt;Device profiling&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Background device signal collection for risk evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/device-self-service/index.html&quot;&gt;Device management&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;User-facing device self-service flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/user-self-service/index.html&quot;&gt;User self-service&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Profile management and account recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/use-cases/magic-links/index.html&quot;&gt;Magic links&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Resuming journeys via deep-linked magic links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/usage/react-native/index.html&quot;&gt;Journey orchestration&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Configuring, starting, and navigating authentication journeys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/customization.html&quot;&gt;SDK customization&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Logging, storage, and network request customization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;How to get started&lt;/h2&gt;
&lt;p&gt;All packages are published to npm under the &lt;code&gt;@ping-identity&lt;/code&gt; scope, so there’s nothing to configure beyond the standard registry.&lt;/p&gt;
&lt;p&gt;The documented baseline targets React Native 0.80.1 and supports iOS 16.0+ and Android API 29+. For more information, refer to &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/compatibility.html&quot;&gt;supported operating systems and browsers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Install the packages the app needs. It is recommended to include the core package alongside the auth packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yarn&lt;/span&gt;&lt;span&gt; add&lt;/span&gt;&lt;span&gt; @ping-identity/rn-oidc&lt;/span&gt;&lt;span&gt; @ping-identity/rn-journey&lt;/span&gt;&lt;span&gt; @ping-identity/rn-core&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Note:&lt;/strong&gt; For teams evaluating the SDK, the sample app is one of the most useful starting points. The repository includes PingSampleApp, and the documentation positions it as the place where the SDK packages are exercised together. Follow our &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/try-it-out/react-native/index.html&quot;&gt;tutorial&lt;/a&gt; to get started!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What’s next&lt;/h2&gt;
&lt;p&gt;Version 1.0 targets teams building on &lt;strong&gt;PingOne Advanced Identity Cloud&lt;/strong&gt; and &lt;strong&gt;PingAM&lt;/strong&gt; — Journey orchestration, OIDC flows, token management, and MFA are all tested and supported against both platforms. If you’re running on either, this release is ready to use today.&lt;/p&gt;
&lt;p&gt;The OIDC package also supports standard redirect-based flows against any OIDC-compliant authorization server, so teams not yet on a Ping Identity platform or that want an in-app browser redirect experience can use the React Native SDK.&lt;/p&gt;
&lt;p&gt;Up next are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;React Native SDK agent skills&lt;/strong&gt;: The goal is to make common setup work less repetitive: scaffold an authentication flow, wire Journey callbacks, configure OIDC, and generate a sample integration from a single prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DaVinci support&lt;/strong&gt;: The plan is to bring the same client and hook model used elsewhere in the SDK to PingOne DaVinci orchestration as that integration expands.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/index.html&quot;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ForgeRock/ping-react-native-sdk&quot;&gt;Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forgerock.github.io/ping-react-native-sdk/index.html&quot;&gt;API documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.R0CCnL-y_pIjk4.webp" medium="image" type="image/webp" width="1480" height="755"/></item><item><title>Securing agentic workflows with token exchange and workload identity</title><link>https://developer.pingidentity.com/blog/securing-agentic-workflows-with-token-exchange-and-workload-identity/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/securing-agentic-workflows-with-token-exchange-and-workload-identity/</guid><description>How to preserve user context and workload accountability across apps, agents, and MCP servers using OAuth token exchange, SPIFFE/SPIRE, and PingFederate.</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/spiffe-token-exchange-for-agent-workloads/&quot;&gt;Federico Carbone’s blog site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;Agentic systems change the identity problem. The user can start the request, but the agent decides how to complete it. It can call tools, invoke other services, inspect intermediate results, and change the plan as the session progresses. That means the important question is no longer only “who is the user?” It is also “which agent or workload acted, on behalf of whom, and why was that action allowed?”&lt;/p&gt;
&lt;p&gt;Traditional identity models typically divide identity into two categories: human users and workloads. Workloads often authenticate with service accounts, API keys, or client credentials. That works for simple automation, but it becomes weak for agentic flows that are delegated, dynamic and non-deterministic. Furthermore, if every downstream call only shows a generic service identity, the audit trail loses the real story. A protected API could see “the agent service called me,” but not that a specific agent acted for a specific user after a specific tool decision.&lt;/p&gt;
&lt;p&gt;That is unsafe because accountability becomes partial. Security teams need to answer who did what, when, and why. Downstream systems need enough context to enforce fine-grained policies. Incident responders should not have to reconstruct a session by stitching together disconnected logs from the browser, portal, agent, and tools. The identity context should travel with the flow. The architecture needs a way to propagate both user identity and actor identity across hops, without collapsing everything into one broad user token or one generic service account.&lt;/p&gt;
&lt;p&gt;This blog uses a dummy telco customer-support chatbot to show how Ping Identity can support a safer model. The business behavior is intentionally simple. The important part is the identity chain across the browser, the web application, the agent, the MCP server, PingFederate, and SPIRE.&lt;/p&gt;
&lt;p&gt;The pattern is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use OAuth 2.0 Token Exchange to mint a new access token at each service boundary.&lt;/li&gt;
&lt;li&gt;Use SPIFFE/SPIRE to give each workload an attested identity.&lt;/li&gt;
&lt;li&gt;Use the workload JSON Web Token SPIFFE Verifiable Identity Document (JWT-SVID) as the token exchange actor token.&lt;/li&gt;
&lt;li&gt;Let each receiving service validate a token minted for its own audience.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gives us user delegation and workload identity in the same flow.&lt;/p&gt;
&lt;h2&gt;Why token exchange&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8693&quot;&gt;RFC 8693&lt;/a&gt; defines OAuth 2.0 Token Exchange. It describes a protocol for requesting and obtaining security tokens from OAuth authorization servers, including tokens that support delegation.&lt;/p&gt;
&lt;p&gt;The important part for this demo is the request model. A client sends a &lt;code&gt;subject_token&lt;/code&gt;, an &lt;code&gt;actor_token&lt;/code&gt;, and asks for a token with a target audience and scope. The &lt;code&gt;subject_token&lt;/code&gt; represents the party on behalf of whom the token is requested, while the &lt;code&gt;actor_token&lt;/code&gt; represents the party receiving delegated rights. That maps naturally to agentic systems.&lt;/p&gt;
&lt;p&gt;The user remains the subject. The portal and agent are actors. Each actor gets only the token it needs for the next hop. The receiving service can validate the audience, scopes, subject, and actor chain instead of guessing intent from a token minted for another resource.&lt;/p&gt;
&lt;p&gt;This matters for three reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auditability&lt;/strong&gt;: If an agent reads payment data, we want logs and tokens to show the user and the workload path, not just a generic service account.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy&lt;/strong&gt;: PingFederate can allow the portal workload to request only an agent-audience token, and allow the agent workload to request only an MCP-audience token. The customer scopes can travel across the chain, but each token is still minted for the next workload. That is more precise than giving a backend service account broad standing access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Containment&lt;/strong&gt;: Reusing a user token across services spreads a bearer credential beyond its intended audience. Using a generic service account loses the user delegation context. Token exchange avoids both problems by minting a new, constrained token for the next resource.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why SPIFFE and SPIRE&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://spiffe.io/docs/latest/spiffe-about/overview/&quot;&gt;SPIFFE&lt;/a&gt; gives software components a standard way to identify themselves. A component can be an API, an agent, a worker, or any service running in the platform. Instead of saying “this is some pod” or “this is some service account,” SPIFFE gives it a stable identity string called a SPIFFE ID, for example &lt;code&gt;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://spiffe.io/docs/latest/spire-about/spire-concepts/&quot;&gt;SPIRE&lt;/a&gt; is one implementation of SPIFFE. Its job is to check what a workload really is and then issue a short-lived credential for it. That check is what “attestation” means. In Kubernetes, SPIRE can look at platform facts such as the namespace, pod labels, and service account. If those facts match the configured rules, SPIRE gives the workload a credential.&lt;/p&gt;
&lt;p&gt;That credential is called an SVID, or SPIFFE Verifiable Identity Document. In simpler terms, it’s a signed identity document for the workload. When the portal gets a JWT-SVID, it’s getting a signed token that says “this workload is the portal service” and includes the portal SPIFFE ID. When the agent gets one, it says “this workload is the agent service.”&lt;/p&gt;
&lt;p&gt;In this project, the portal and agent request JWT-SVIDs from the SPIFFE API. Those JWT-SVIDs are short-lived workload credentials. The services send them to PingFederate as &lt;code&gt;actor_token&lt;/code&gt; values during token exchange.&lt;/p&gt;
&lt;p&gt;That gives PingFederate two independent facts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;subject_token&lt;/code&gt; is the incoming OAuth access token representing the user.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;actor_token&lt;/code&gt; is the SPIFFE JWT-SVID proving which workload is asking for the next token.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No shared secret is needed between the workload and PingFederate. The trust is based on PingFederate validating the JWT-SVID issuer, signature, audience, and SPIFFE subject.&lt;/p&gt;
&lt;h2&gt;Demo architecture&lt;/h2&gt;
&lt;p&gt;The browser uses Authorization Code + PKCE with PingFederate. After that, the server-side path uses token exchange with SPIFFE JWT-SVID actor tokens.&lt;/p&gt;
&lt;p&gt;Each token exchange produces a new access token targeted at the next service in the chain, with a different audience while preserving the delegated user identity.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; LR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    B&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Browser SPA&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Portal API&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    A&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Agent&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    M&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;MCP Server&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PF&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;PingFederate&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    S&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;SPIRE / SPIFFE&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    B &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; authorize + PKCE &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PF&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PF &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; User Token&amp;lt;br/&amp;gt;sub=User&amp;lt;br/&amp;gt;act=-&amp;lt;br/&amp;gt;aud=Portal API &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; B&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    B &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; API call (User Token) &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; P&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; fetch JWT-SVID &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; S&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; token exchange&amp;lt;br/&amp;gt;subject=User Token&amp;lt;br/&amp;gt;actor=Portal JWT-SVID &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PF&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PF &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; Agent Token&amp;lt;br/&amp;gt;sub=User&amp;lt;br/&amp;gt;act=Portal API&amp;lt;br/&amp;gt;aud=Agent &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; P&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; A2A call (Agent Token) &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; A&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    A &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; fetch JWT-SVID &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; S&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    A &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; token exchange&amp;lt;br/&amp;gt;subject=Agent Token&amp;lt;br/&amp;gt;actor=Agent JWT-SVID &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PF&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PF &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; MCP Token&amp;lt;br/&amp;gt;sub=User&amp;lt;br/&amp;gt;act=Agent&amp;lt;br/&amp;gt;aud=MCP Server &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; A&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    A &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; MCP tool call (MCP Token) &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; M&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main components are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Browser SPA&lt;/strong&gt;: Signs the user in with PingFederate and calls the portal API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portal API&lt;/strong&gt;: Validates the browser token, exchanges it for an agent token, and forwards the user request to the agent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt;: Validates the agent token, chooses MCP tools, exchanges the token again for the MCP audience, and calls the MCP server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP server&lt;/strong&gt;: Validates the MCP token and enforces tool scopes before returning customer data.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;PingFederate: issues the User Token, Agent Token, and MCP Token, and validates the SPIFFE JWT-SVID actor token during token exchange.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SPIRE&lt;/strong&gt;: Issues JWT-SVIDs to Kubernetes workloads through the SPIFFE Workload API.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Expected flow&lt;/h2&gt;
&lt;p&gt;A normal support request follows this path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user signs in through the Browser SPA.&lt;/li&gt;
&lt;li&gt;PingFederate issues a User Token for the Portal API to the Browser SPA.&lt;/li&gt;
&lt;li&gt;The Browser SPA calls &lt;code&gt;POST /api/chat&lt;/code&gt; on the Portal API with the User Token.&lt;/li&gt;
&lt;li&gt;The Portal API validates the User Token.&lt;/li&gt;
&lt;li&gt;The Portal API fetches a portal JWT-SVID from SPIRE / SPIFFE.&lt;/li&gt;
&lt;li&gt;The Portal API sends a token exchange request to PingFederate with the User Token as the subject token and the portal JWT-SVID as the actor token.&lt;/li&gt;
&lt;li&gt;PingFederate issues an Agent Token to the Portal API for the Agent audience.&lt;/li&gt;
&lt;li&gt;The Portal API calls &lt;code&gt;POST /a2a/message&lt;/code&gt; on the Agent with the Agent Token.&lt;/li&gt;
&lt;li&gt;The Agent validates the Agent Token and chooses MCP tools.&lt;/li&gt;
&lt;li&gt;The Agent fetches an agent JWT-SVID from SPIRE / SPIFFE.&lt;/li&gt;
&lt;li&gt;The Agent sends a token exchange request to PingFederate with the Agent Token as the subject token and the agent JWT-SVID as the actor token.&lt;/li&gt;
&lt;li&gt;PingFederate issues an MCP Token to the Agent for the MCP Server audience.&lt;/li&gt;
&lt;li&gt;The Agent calls the MCP Server with the MCP Token.&lt;/li&gt;
&lt;li&gt;The MCP Server validates audience and scopes before returning profile or payment data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The video below shows the demo from the application point of view. It starts with a signed-in user invoking the chatbot, then opens the token flow view where the tokens are surfaced for demo purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;At each hop, the subject remains the user who initiated the action.&lt;/li&gt;
&lt;li&gt;Each exchange uses the current access token as the subject token and the workload JWT-SVID as the actor token.&lt;/li&gt;
&lt;li&gt;Scopes are reduced as the request moves from the Portal API to the Agent and then to the MCP Server.&lt;/li&gt;
&lt;/ul&gt;

  
  Your browser does not support the video tag.

&lt;h2&gt;Workload identity model&lt;/h2&gt;
&lt;p&gt;The Kubernetes manifest defines service accounts for the three workloads and &lt;code&gt;ClusterSPIFFEID&lt;/code&gt; resources that map pods to SPIFFE IDs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/mcp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The portal and agent mount the SPIFFE Workload API socket:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;volumeMounts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  - &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;spiffe-workload-api&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    mountPath&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/spiffe-workload-api&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    readOnly&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  - &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;spiffe-workload-api&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    csi&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      driver&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;csi.spiffe.io&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      readOnly&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the socket is mounted, the application can call the SPIFFE Workload API from inside the pod and ask for a JWT-SVID. The requested JWT-SVID audience is the value PingFederate expects for token exchange validation.&lt;/p&gt;
&lt;p&gt;The client IDs intentionally match the SPIFFE IDs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;API_OAUTH_CLIENT_ID=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;AGENT_OAUTH_CLIENT_ID=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;SPIFFE_JWT_SVID_AUDIENCE=pingfederate-token-exchange&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives PingFederate a direct mapping between the OAuth token-exchange client and the attested workload identity.&lt;/p&gt;
&lt;h2&gt;Fetching the JWT-SVID&lt;/h2&gt;
&lt;p&gt;The software fetches a JWT-SVID before calling the PingFederate token endpoint. The important request fields are the SPIFFE ID and the audience. In the demo, this is done with the SPIFFE/SPIRE Python SDK through &lt;code&gt;WorkloadApiClient&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; spiffe &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; WorkloadApiClient&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; spiffe.spiffe_id.spiffe_id &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; SpiffeId&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;socket_path &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;unix:/spiffe-workload-api/spire-agent.sock&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;audience &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;pingfederate-token-exchange&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;portal_spiffe_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Retrieve the JWT-SVID from the SPIFFE Workload API using the SPIFFE/SPIRE SDK.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt; WorkloadApiClient(&lt;/span&gt;&lt;span&gt;socket_path&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;socket_path, &lt;/span&gt;&lt;span&gt;default_timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5.0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; client:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    svid &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; client.fetch_jwt_svid(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        audience&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{audience},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        subject&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;SpiffeId(portal_spiffe_id),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5.0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;portal_jwt_svid &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; svid.token&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decoded, the portal JWT-SVID looks conceptually like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://oidc-discovery.ping.demo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;pingfederate-token-exchange&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;exp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1782774000&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent gets its own JWT-SVID with its own SPIFFE ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://oidc-discovery.ping.demo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;pingfederate-token-exchange&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;exp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1782774000&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are not API access tokens. They are workload identity documents used as proof of the actor during token exchange.&lt;/p&gt;
&lt;h2&gt;Building the token exchange request&lt;/h2&gt;
&lt;p&gt;The two server-side exchanges use the same RFC 8693 request shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Portal to agent&lt;/strong&gt;: the subject token is the browser access token and the actor token is the portal JWT-SVID.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent to MCP&lt;/strong&gt;: the subject token is the agent access token and the actor token is the agent JWT-SVID.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, the portal-to-agent exchange is a form post to the PingFederate token endpoint. The incoming browser access token becomes the &lt;code&gt;subject_token&lt;/code&gt;, and the portal JWT-SVID becomes the &lt;code&gt;actor_token&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; urllib.parse &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; urlencode&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; httpx&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;token_endpoint &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;https://pingfederate.example.com/as/token.oauth2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;portal_client_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;agent_resource &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;agent_scopes &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;customer:profile:read customer:payments:read&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;body &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; urlencode({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;grant_type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;urn:ietf:params:oauth:grant-type:token-exchange&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;subject_token&quot;&lt;/span&gt;&lt;span&gt;: browser_access_token,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;subject_token_type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;urn:ietf:params:oauth:token-type:access_token&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;actor_token&quot;&lt;/span&gt;&lt;span&gt;: portal_jwt_svid,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;actor_token_type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;urn:ietf:params:oauth:token-type:jwt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: agent_scopes,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;resource&quot;&lt;/span&gt;&lt;span&gt;: agent_resource,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;client_id&quot;&lt;/span&gt;&lt;span&gt;: portal_client_id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;headers &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;accept&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;content-type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/x-www-form-urlencoded&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; with&lt;/span&gt;&lt;span&gt; httpx.AsyncClient(&lt;/span&gt;&lt;span&gt;timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; client:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; client.post(token_endpoint, &lt;/span&gt;&lt;span&gt;content&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;body, &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;headers)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    response.raise_for_status()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;agent_token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.json()[&lt;/span&gt;&lt;span&gt;&quot;access_token&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Exchange one: portal to agent&lt;/h3&gt;
&lt;p&gt;The browser starts with a user token for the portal API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://pingfederate.example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;customer-support-agent-portal-api&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;openid profile customer-support-agent:portal-api:chat customer-support-agent:customer:profile:read customer-support-agent:customer:payments:read&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;customer_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;cust-1001&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before the portal calls the agent, it exchanges that browser token for an agent token. The request uses:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;subject_token=&amp;lt;browser access token&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;actor_token=&amp;lt;portal JWT-SVID&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;audience=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;scope=customer-support-agent:customer:profile:read customer-support-agent:customer:payments:read&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;client_id=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issued token is for the agent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://pingfederate.example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;customer-support-agent:customer:profile:read customer-support-agent:customer:payments:read&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;client_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent validates this token against its own SPIFFE ID audience and rejects any token not minted for the agent.&lt;/p&gt;
&lt;h3&gt;Exchange two: agent to MCP&lt;/h3&gt;
&lt;p&gt;When the agent needs customer data, it exchanges the agent token for an MCP token. The request uses:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;subject_token=&amp;lt;agent access token&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;actor_token=&amp;lt;agent JWT-SVID&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;audience=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/mcp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;scope=customer-support-agent:customer:profile:read customer-support-agent:customer:payments:read&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;client_id=spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issued token is for the MCP server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://pingfederate.example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/mcp&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;customer-support-agent:customer:profile:read customer-support-agent:customer:payments:read&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;client_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/agent&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;spiffe://ping.demo/ns/spiffe-token-exchange-demo/sa/portal&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MCP server is now seeing a token that was minted for the MCP resource, not a reused portal or agent token. The token also preserves the actor chain: the user is still the subject, the agent is the immediate actor, and the portal remains visible as the previous actor. That chain can be used for audit logs and for fine-grained policy decisions such as “allow payment data only when this user delegated through this agent workload.”&lt;/p&gt;
&lt;p&gt;The MCP server still enforces authorization at the tool boundary. The profile tool requires &lt;code&gt;customer-support-agent:customer:profile:read&lt;/code&gt;. The payment tool requires &lt;code&gt;customer-support-agent:customer:payments:read&lt;/code&gt;. Token exchange gets the request to the right resource with the right scopes; the resource server still validates and enforces them.&lt;/p&gt;
&lt;h2&gt;Token exchange logic in PingFederate&lt;/h2&gt;
&lt;p&gt;PingFederate is the authorization server that validates the incoming user token, validates the workload actor token, maps the relevant attributes, evaluates token-exchange policy, and issues the next access token. One important point is that the subject token and actor token can come from different issuers. In this demo, the subject token is a PingFederate access token and the actor token is a SPIRE JWT-SVID. PingFederate can validate each token independently, then combine the resulting user and workload context in the token exchange policy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; LR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ST&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;b&amp;gt;Subject token validator&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(validates user token&amp;lt;br/&amp;gt;issuer, signature, audience, scopes)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    AT&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;b&amp;gt;Actor token validator&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(validates workload JWT-SVID&amp;lt;br/&amp;gt;issuer, signature, audience, SPIFFE ID)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    POLICY&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;b&amp;gt;Token Exchange Processor Policy&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(maps validators&amp;lt;br/&amp;gt;and inbound attributes)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ACCESS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;b&amp;gt;Access token mapping&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(sets claims, scopes&amp;lt;br/&amp;gt;and actor chain)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ISSUER&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;b&amp;gt;Token issuer&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(issues next-hop&amp;lt;br/&amp;gt;access token)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ST &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; user claims &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; POLICY&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    AT &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; workload claims &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; POLICY&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    POLICY &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; inbound claims &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; ACCESS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ACCESS &lt;/span&gt;&lt;span&gt;--&amp;gt;|&lt;/span&gt;&lt;span&gt;&quot; token contract &quot;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; ISSUER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At a high level, the demo needs three OAuth clients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A browser SPA client for Authorization Code + PKCE.&lt;/li&gt;
&lt;li&gt;A portal token-exchange client whose client ID is the portal SPIFFE ID.&lt;/li&gt;
&lt;li&gt;An agent token-exchange client whose client ID is the agent SPIFFE ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GitHub repository will contain the PingFederate configuration details. The main PingFederate OAuth configuration area is documented in the &lt;a href=&quot;https://docs.pingidentity.com/pingfederate/latest/administrators_reference_guide/pf_oauth_config.html&quot;&gt;PingFederate OAuth configuration guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;When to use this pattern&lt;/h2&gt;
&lt;p&gt;This pattern is useful when a user-facing app calls an intermediate service, and that service calls more protected backend tools or APIs. It is especially useful when those intermediate services are workloads running in Kubernetes and can receive SPIFFE identities automatically.&lt;/p&gt;
&lt;p&gt;Use it when you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Resource-specific access tokens at every service boundary.&lt;/li&gt;
&lt;li&gt;Workload identity without distributing static client secrets to pods.&lt;/li&gt;
&lt;li&gt;A visible actor chain for delegated calls.&lt;/li&gt;
&lt;li&gt;Tighter control over which workload can request which downstream audience and scopes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not use it as a substitute for API authorization. The final resource server still needs to validate tokens and enforce scopes, claims, and object-level rules. Token exchange gives the resource a better token to validate; it does not remove the need to validate it.&lt;/p&gt;
&lt;h2&gt;Key takeaways&lt;/h2&gt;
&lt;p&gt;The point of the demo is not that an agent can call a tool. The point is that every call crosses the boundary with the right identity material.&lt;/p&gt;
&lt;p&gt;The browser token is for the portal. The portal proves its workload identity with a SPIFFE JWT-SVID and gets an agent token. The agent proves its workload identity with its own JWT-SVID and gets an MCP token. The MCP server receives a token minted for its audience and enforces tool scopes before returning data.&lt;/p&gt;
&lt;p&gt;That is the core pattern: user delegation travels through OAuth token exchange, workload identity travels through SPIFFE, and each service validates only the token meant for itself.&lt;/p&gt;
&lt;p&gt;One thing this article does not solve is proof of possession. The exchanged tokens are still bearer tokens. A follow-up article should explore binding issued tokens to a key or mTLS channel so possession of the token string alone is not enough to use it.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OAuth 2.0 Token Exchange: &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8693&quot;&gt;RFC 8693&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SPIFFE overview: &lt;a href=&quot;https://spiffe.io/docs/latest/spiffe-about/overview/&quot;&gt;SPIFFE documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SPIRE concepts: &lt;a href=&quot;https://spiffe.io/docs/latest/spire-about/spire-concepts/&quot;&gt;SPIRE documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingFederate OAuth configuration: &lt;a href=&quot;https://docs.pingidentity.com/pingfederate/latest/administrators_reference_guide/pf_oauth_config.html&quot;&gt;PingFederate Server documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href=&quot;https://github.com/carbonefederico/ai-spiffe-token-exchange-demo&quot;&gt;carbonefederico/ai-spiffe-token-exchange-demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CqO9QvHp_ZdkO9B.webp" medium="image" type="image/webp" width="2752" height="1536"/></item><item><title>Ping CLI 1.0: A Command Line Utility for the Ping Platform</title><link>https://developer.pingidentity.com/blog/ping-cli-v1-0-a-command-line-utility-for-the-ping-platform/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/ping-cli-v1-0-a-command-line-utility-for-the-ping-platform/</guid><description>Ping CLI has reached general availability — learn what it does, how it supports config-as-code and CI/CD for PingOne services, and where it is headed.</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We’re excited to share that &lt;strong&gt;Ping CLI has reached version 1.0&lt;/strong&gt;, a long-term supported, generally available release. Ping CLI is a command-line tool that connects to multiple Ping Identity services simultaneously, allowing you to manage them through one consistent, developer-first terminal interface.&lt;/p&gt;
&lt;p&gt;Ping Identity has a broad platform, and Ping CLI enables standard management patterns through a generalized, consistent terminal interface. Use it to promote configuration from staging to production, validate that your apps are set up correctly before a release, script a repeatable setup, or explore an API interactively with common conventions. For developers who live in the terminal, it means one management interface across your configured services, a logical set of commands, and configuration you can export, version, and share.&lt;/p&gt;
&lt;p&gt;This article is an introduction and will highlight what Ping CLI is, what you can do with it right now, and where it’s headed.&lt;/p&gt;
&lt;h2&gt;Where we’ve come from&lt;/h2&gt;
&lt;p&gt;CLIs at Ping have taken many forms and have evolved incrementally for their individual use cases. As our platform capabilities have expanded and become better integrated, we saw the need for a single terminal experience that allows developers to manage configuration from the terminal, as well as bundle configuration into configuration-as-code packages and reliably deploy those packages to higher environments.&lt;/p&gt;
&lt;p&gt;We started the unified CLI journey with Ping CLI in late 2024 as an evolution of &lt;code&gt;pingctl&lt;/code&gt; and, in its initial experimental releases, you gave us valuable feedback that helped us steer toward our current milestone release.&lt;/p&gt;
&lt;p&gt;Version 1.0 represents our commitment to our developer community: to meet developers where they’re at, whether that’s in the terminal, the web admin consoles, or using agents. The CLI is now fully supported and can be built into workflows with confidence.&lt;/p&gt;
&lt;h2&gt;What it is used for&lt;/h2&gt;
&lt;p&gt;The CLI serves three main use cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Developers in the terminal&lt;/li&gt;
&lt;li&gt;Deterministic automation and CI/CD&lt;/li&gt;
&lt;li&gt;Headless, AI-first use cases&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here we briefly discuss all three, but keep an eye out for future deep-dive articles.&lt;/p&gt;
&lt;h3&gt;Developers in the terminal&lt;/h3&gt;
&lt;p&gt;The CLI is designed to be intuitive and easy to use for developers that live in the terminal. If you prefer the terminal to the browser for quick (or extensive) configuration changes and shell scripting for rapid prototyping and personal automation, then the CLI presents an efficient alternative to administration in the web console.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; use the CLI’s shell completion to quickly build commands with Tab autocomplete.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Deterministic automation and CI/CD&lt;/h3&gt;
&lt;p&gt;GitOps-based CI/CD is an industry standard method of promoting configuration between environments in an automated, auditable, and highly governed way. Where customers have invested in GitOps CI/CD pipelines, there has to be a repeatable, idempotent way to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Export Ping Identity configuration as code.&lt;/li&gt;
&lt;li&gt;Organize exported configuration as code within the filesystem.&lt;/li&gt;
&lt;li&gt;Version control within source control (such as Git).&lt;/li&gt;
&lt;li&gt;During CI/CD workflow execution, apply that versioned configuration to the next environment in the deployment pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is no one-size-fits-all approach to CI/CD, but there are industry-standard patterns of use, so the CLI provides the toolkit to natively integrate with a wide range of CI/CD workflow orchestrators, with both imperative (“these are the actions to take to get to the end state”) and declarative (“this is the end state I want, take whatever actions are needed”) methods of configuration management with the ability to integrate with OpenTelemetry collectors for deep observability.&lt;/p&gt;
&lt;h3&gt;Headless, AI-first use cases&lt;/h3&gt;
&lt;p&gt;Ping CLI is designed to be natively understood by coding and configuration agents. The ability to understand and use CLIs is inherent in the training of many large language and small language (LLM and SLM) models, meaning they know how to read and run CLIs and interpret the output, to determine the actions to take.&lt;/p&gt;
&lt;p&gt;By logically laying out the command structure with brief but effective &lt;code&gt;--help&lt;/code&gt; text, agents can crawl the CLI to be able to compose commands to achieve an outcome. When paired with developer documentation (such as the &lt;a href=&quot;https://docs.pingidentity.com/solution-guides/htg_overview.html&quot;&gt;Developer Use Cases&lt;/a&gt;), the agent can compose commands, with data payloads, to accelerate the development of use cases.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Use the bundled &lt;code&gt;ping-cli-usage&lt;/code&gt; Agent Skill so the agent doesn’t need to crawl the &lt;code&gt;--help&lt;/code&gt; texts to understand how to use the CLI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Agents can use the Ping CLI to augment the CI/CD and configuration promotion workflows, by exporting configuration, raising requests to change the configuration-as-code within the GitOps pipeline with Pull Requests (as coding agents do today), and orchestrating the CI/CD workflows to reduce repetitive work by DevOps developers and administrators.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Refer to &lt;a href=&quot;https://developer.pingidentity.com/build-with-ai&quot;&gt;Build with AI developer documentation&lt;/a&gt; for more information on how agents can use the Ping CLI alongside Agent Skills and documentation for agents.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What you can do today&lt;/h2&gt;
&lt;p&gt;We’re on our journey to build out the CLI capability and build a truly unified CLI experience that allows rich management features covering all of Ping Identity’s product capabilities. We’ve started by releasing a solid foundation, with configuration management features for &lt;strong&gt;PingOne&lt;/strong&gt; and universal services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DaVinci&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne MFA&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Protect&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Verify&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Authorize&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Credentials&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s take a look at some of the new features available in 1.0!&lt;/p&gt;
&lt;h3&gt;Interactive guided setup&lt;/h3&gt;
&lt;p&gt;The Ping CLI now provides a guided setup process. Use &lt;code&gt;pingcli init&lt;/code&gt; to walk you through first-time configuration, including connecting products and services and setting some initial preferred settings.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you’re looking to just connect one set of products, each platform has its own guided setup process. To specifically connect PingOne with the guided setup interface, use &lt;code&gt;pingcli pingone init&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The &lt;code&gt;pingcli init&lt;/code&gt; command&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Authenticate across multiple services&lt;/h3&gt;
&lt;p&gt;We’ve streamlined the login process for developers using interactive logins using the browser. After connecting products to the CLI through the interactive setup, login for all can be performed with a single command.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The &lt;code&gt;pingcli auth login&lt;/code&gt; command&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The simplicity of managing admin sessions, as one CLI session, allows developers to spend less time authenticating and more time getting the job done.&lt;/p&gt;
&lt;p&gt;Ping CLI supports the login methods you’d expect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service accounts for automation&lt;/strong&gt;: for use with CI/CD pipelines and fully automated workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive login flows with silent token refresh&lt;/strong&gt;: where supported, interactive logins are for developers using the CLI directly in the terminal. IAM administrators can apply strong policies to CLI users, including the use of admin MFA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where available, the CLI stores tokens safely in your OS keychain, or ephemerally where keychains are not available (such as when running in an OCI container).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Each platform has its own auth management. If you’re looking to just manage PingOne without any other connected product, use &lt;code&gt;pingcli pingone auth login&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Manage configuration across products and services&lt;/h3&gt;
&lt;p&gt;Each product and service gets its own command namespace. PingOne lives under &lt;code&gt;pingcli pingone&lt;/code&gt;, and each resource exposes a consistent set of verbs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create&lt;/code&gt;: to create new resources within an environment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apply&lt;/code&gt;: an idempotent way of maintaining a resource’s configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt;: to return the full configuration of a resource by its identifier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt;: to return multiple resources of the same type (with a filter, where the API allows)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replace&lt;/code&gt;: to overwrite a resource with new configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt;: to remove a resource from the environment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;: generates a JSON body skeleton as a starting point for manual configuration-as-code development&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Most commands have aliases to make life easier, giving users and agents command patterns that suit them best. For example, &lt;code&gt;pingcli p1&lt;/code&gt; is an alias of &lt;code&gt;pingcli pingone&lt;/code&gt;. The &lt;code&gt;create&lt;/code&gt; verb also has an alias of &lt;code&gt;new&lt;/code&gt;, the &lt;code&gt;update&lt;/code&gt; verb has an alias of &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt; has an alias of &lt;code&gt;ls&lt;/code&gt;, among others. Try out what suits you. If we miss an obvious command alias, tell us with the &lt;code&gt;pingcli feedback&lt;/code&gt; command.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The &lt;code&gt;pingcli pingone application&lt;/code&gt; example commands&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The universal services (MFA, Verify, Protect, Credentials, Authorize, and DaVinci) are addressable directly by service, or as part of their platform for convenience:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Example showing how DaVinci flows are managed&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The same command shapes carry across resources, services, and products, so the muscle memory you build in one place transfers everywhere. Resources deep-nest naturally too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pingcli&lt;/span&gt;&lt;span&gt; pingone&lt;/span&gt;&lt;span&gt; applications&lt;/span&gt;&lt;span&gt; sop-assignments&lt;/span&gt;&lt;span&gt; list&lt;/span&gt;&lt;span&gt; --environment-id&lt;/span&gt;&lt;span&gt; $ENVIRONMENT_ID &lt;/span&gt;&lt;span&gt;--application-id&lt;/span&gt;&lt;span&gt; $APPLICATION_ID&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pingcli&lt;/span&gt;&lt;span&gt; pingone&lt;/span&gt;&lt;span&gt; groups&lt;/span&gt;&lt;span&gt; group-nestings&lt;/span&gt;&lt;span&gt; list&lt;/span&gt;&lt;span&gt; --environment-id&lt;/span&gt;&lt;span&gt; $ENVIRONMENT_ID &lt;/span&gt;&lt;span&gt;--group-id&lt;/span&gt;&lt;span&gt; $GROUP_ID&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Profiles for every environment&lt;/h3&gt;
&lt;p&gt;Real workflows span sandboxes, development, testing, staging, and production. Some users will need to be operating on multiple environments, or switching contexts depending on what is needed in the moment.&lt;/p&gt;
&lt;p&gt;Configuration profiles let you keep those configurations side by side and allows quick switching between them globally or per command:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Example showing configuration profile use&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Configuration is persisted in (and can be loaded by) a readable YAML file, and every setting can be overridden by an environment variable, which is ideal for easily injecting configuration and secrets into CI/CD pipelines at runtime.&lt;/p&gt;
&lt;h3&gt;Make raw API calls when you need them&lt;/h3&gt;
&lt;p&gt;The managed resources cover the common cases and are being built out over the next few releases, but sometimes you need the full surface of an API. This is especially useful if you’re using a prerelease product feature that’s not yet built in as a native command, or you need more control than a native resource command can provide.&lt;/p&gt;
&lt;p&gt;Each product exposes an &lt;code&gt;api&lt;/code&gt; command giving you a cURL-like experience with authentication, base URL, and environment details filled in for you automatically:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Example showing custom API commands&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Output built for scripting&lt;/h3&gt;
&lt;p&gt;Ping CLI speaks both human and machine. Every command accepts &lt;code&gt;-O/--output-format&lt;/code&gt; with four modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text&lt;/code&gt;: The default, formatted for reading at the terminal.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;json&lt;/code&gt;: A single structured document, ideal for &lt;code&gt;jq&lt;/code&gt; or JMESPath queries using the &lt;code&gt;--query&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ndjson&lt;/code&gt;: Newline-delimited JSON (one record per line), ideal for &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, and for streaming and sending to log pipelines.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ndjson-typed&lt;/code&gt;: Newline-delimited JSON with type metadata per record, ideal for &lt;code&gt;jq&lt;/code&gt; and sending to log pipelines that need a consistent structure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For extracting exactly the fields you need, &lt;code&gt;--query&lt;/code&gt; takes a JMESPath expression directly rather than piping to another tool. For transformations that go beyond what JMESPath supports, pipe to &lt;code&gt;jq&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Using the &lt;code&gt;--query&lt;/code&gt; parameter and piping output to &lt;code&gt;jq&lt;/code&gt; example&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For pipelines that must react to results, &lt;code&gt;--no-color&lt;/code&gt; keeps logs clean, while &lt;code&gt;--detailed-exitcode&lt;/code&gt; distinguishes success (&lt;code&gt;0&lt;/code&gt;), error (&lt;code&gt;1&lt;/code&gt;), and success-with-warnings (&lt;code&gt;2&lt;/code&gt;) so a CI step can branch on the outcome as well as text.&lt;/p&gt;
&lt;h3&gt;Quality-of-life extras&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shell completion&lt;/strong&gt;: &lt;code&gt;pingcli completion zsh&lt;/code&gt; (also &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;fish&lt;/code&gt;, &lt;code&gt;powershell&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI agent skills&lt;/strong&gt;: &lt;code&gt;pingcli agent-skills install&lt;/code&gt; / &lt;code&gt;list&lt;/code&gt; for teams building agent-assisted workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenTelemetry integration&lt;/strong&gt;: For deep observability into CLI actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Ping CLI ships as a signed release for macOS, Linux, and Windows across common architectures, and as a Docker image. To get started for PingOne:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Follow the PingOne getting started guide in the &lt;a href=&quot;https://developer.pingidentity.com/pingcli/latest/pingone/getting-started.html&quot;&gt;CLI documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Run the guided initialization walkthrough (which authenticates you at the end): &lt;code&gt;pingcli init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;List the environments you’re able to view: &lt;code&gt;pingcli pingone environments list&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Full installation instructions, the complete command reference, and configuration guides live on the &lt;a href=&quot;https://developer.pingidentity.com/pingcli/&quot;&gt;Ping Identity Developer Portal&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What’s next&lt;/h2&gt;
&lt;p&gt;Version 1.0 is a commitment to stability: a foundation you can build automation on with confidence. But it’s a milestone, not a destination. The vision is a genuinely universal tool, one that speaks to every Ping service and platform with the same fluency it speaks to PingOne today.&lt;/p&gt;
&lt;p&gt;As a preview, in the pipeline we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced Identity Cloud configuration management.&lt;/li&gt;
&lt;li&gt;PingFederate configuration management (for both private tenant and software).&lt;/li&gt;
&lt;li&gt;Defined declarative import/export tools.&lt;/li&gt;
&lt;li&gt;Refined processes for exporting and generating configuration-as-code (as an evolution of the experimental &lt;code&gt;platform export&lt;/code&gt; command in Ping CLI version 0.8).&lt;/li&gt;
&lt;li&gt;Enhanced headless AI features, enabling more streamlined use with configuration agents.&lt;/li&gt;
&lt;li&gt;The ability to introspect environments and debug issues.&lt;/li&gt;
&lt;li&gt;The ability to extend with custom plugins.
And more to come.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Can we improve? Your feedback matters&lt;/h2&gt;
&lt;p&gt;Keep an eye out for future releases that enhance the capabilities of the CLI even further.
Your real-world feedback matters. As you use the Ping CLI, tell us what you need.&lt;/p&gt;
&lt;p&gt;Is there a command alias you’re finding is missing? Is there a feature you’d like included? Is there a use case we can improve upon?&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;pingcli feedback&lt;/code&gt; to reach us directly, or &lt;a href=&quot;https://github.com/pingidentity/pingcli/issues&quot;&gt;open an issue on GitHub&lt;/a&gt; to track a request or vote on features.&lt;/p&gt;
&lt;h2&gt;And finally&lt;/h2&gt;
&lt;p&gt;Look out for upcoming articles where we deep dive into Ping CLI with headless, AI-first configuration using agents, building a CI/CD workflow with Ping CLI, and generating configuration-as-code. Welcome to Ping CLI 1.0!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Used the CLI to build something great? Tell the community! Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CgtFtVRF_ZIPsNS.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Multi-Brand administration in PingOne with new group-level role assignments</title><link>https://developer.pingidentity.com/blog/introducing-pingone-administrator-group-role-assignments/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/introducing-pingone-administrator-group-role-assignments/</guid><description>How group-level role assignments in PingOne solve delegated administration challenges for multi-brand consumer identity deployments.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As you scale PingOne to support richer, multi-brand experiences, a familiar question tends to pop up: &lt;strong&gt;how do you delegate administration cleanly when the same end user belongs to many different business lines?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this post, we’ll walk through what &lt;strong&gt;group-level assignments of administrative roles&lt;/strong&gt; are, how they behave in PingOne, and the kinds of real-world problems they’re designed to solve.&lt;/p&gt;
&lt;h2&gt;Background: How PingOne administration worked before group level assignment&lt;/h2&gt;
&lt;p&gt;PingOne uses a hierarchy of &lt;strong&gt;organization → environment → population / environment → application&lt;/strong&gt; to determine administration over different resources as shown below, before the new group-level assignment type.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;PingOne administration hierarchy — role assignments before group-level support&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A few important concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Administrator environment&lt;/strong&gt;: Each organization comes with a pre-created “administrators” environment. This is where we recommend managing platform administrators, separate from end users. (There are cases where delegated admins should live alongside end users)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in vs custom roles&lt;/strong&gt;: Built-in roles cover common platform responsibilities (for example, Identity Data Admin and Help Desk Admin), while custom roles let you combine granular permissions to reach least privilege.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Levels&lt;/strong&gt;: Roles can be assigned at different levels (organization, environment, population, and — now — groups) so that an admin’s power is limited to exactly what they should manage. This can be done across environments so that a single admin identity has roles over other environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Today, &lt;strong&gt;population-level roles&lt;/strong&gt; are the main way to segment administrator access to different user communities (for example, employees and contractors, or region A and region B) where a user can only exist in a single population. That works well for many B2B and workforce scenarios, but it starts to show its limits in some high-scale consumer use cases.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Learn more in &lt;a href=&quot;https://developer.pingidentity.com/blog/delegated-administration-in-pingone/&quot;&gt;Delegated Administration in PingOne&lt;/a&gt; for a full background on delegated admin and a breakdown of Populations and Groups in PingOne.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The customer problem: shared identity, many brands&lt;/h2&gt;
&lt;p&gt;If a large media company runs many consumer applications under different sub-brands, each brand has its own app and each app has its &lt;strong&gt;own help desk&lt;/strong&gt;, but &lt;strong&gt;end users sign on with a single shared credential across all of them&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;They need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let each brand’s help desk reset passwords, manage MFA devices, and troubleshoot issues &lt;strong&gt;only for their brand’s users&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Avoid creating &lt;strong&gt;separate identities per brand&lt;/strong&gt;, because the user experience should be “one login, many apps”.&lt;/li&gt;
&lt;li&gt;Avoid duplicating users into separate populations just to drive administration, because users commonly span multiple brands and populations aren’t designed for this overlap.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, they need &lt;strong&gt;delegated administration segmented by groups&lt;/strong&gt;, not just by populations.&lt;/p&gt;
&lt;h2&gt;How group-level roles behave&lt;/h2&gt;
&lt;p&gt;With group-level assignment, the goal is for the model to feel predictable and intuitive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If an admin has a role &lt;strong&gt;over a population&lt;/strong&gt;, and a user belongs to that population, the admin can manage that user — just like today.&lt;/li&gt;
&lt;li&gt;If an admin has a role &lt;strong&gt;over a group&lt;/strong&gt;, and the user is in that group, the admin can manage that user — even if the user is also in other groups or populations.&lt;/li&gt;
&lt;li&gt;If the admin has &lt;strong&gt;both&lt;/strong&gt; population and group-level assignments, the effective access is &lt;strong&gt;the union&lt;/strong&gt;: if the user matches &lt;em&gt;either&lt;/em&gt; level, the admin can manage them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practically speaking: &lt;strong&gt;it’s enough for the admin to have an assignment at either the population level or group level to manage a given user&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is exactly what customers like the multi-brand help desk scenario need: “if this user is in any of &lt;em&gt;my&lt;/em&gt; brand groups, I should be able to help them, even if they also belong to other brands.”&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;PingOne administration hierarchy with group-level role assignment support added&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Tackling user creation&lt;/h2&gt;
&lt;p&gt;Group-level assignments introduced a chicken and egg problem: how can an admin create a user and then manage them when in PingOne you cannot assign a user to a group at creation time?&lt;/p&gt;
&lt;p&gt;To solve this problem, we’ve introduced the ability to assign a user to a static group when creating them. This ensures that if an admin needs to create a user manually, they can do so and still manage them afterwards.&lt;/p&gt;
&lt;p&gt;There are some caveats to keep in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If an admin creates a user in a group (given that they have a role assignment over that group) that is associated with a population, the user is created in that population.&lt;/li&gt;
&lt;li&gt;If an admin created a user in a group not associated with a population, then the user is created in the default population if there is one.
&lt;ul&gt;
&lt;li&gt;If there is no default population (not recommended) and the admin is in the same environment, the user is created in the same population as the admin.&lt;/li&gt;
&lt;li&gt;In the event the admin is not in the same environment, user creation will fail.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In most cases the above isn’t a problem, but it is important to understand in more complex organizations.&lt;/p&gt;
&lt;h2&gt;Some example scenarios&lt;/h2&gt;
&lt;p&gt;With group-level assignments, you can model administration much more closely to how your business is structured.&lt;/p&gt;
&lt;h3&gt;1. Brand help desks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A media company runs multiple fan sites and apps under different artist or label brands. Each brand has its own help desk team, but fans use the same account everywhere.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The multi-brand identity problem — one user identity, multiple brand help desks, separate support boundaries needed&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to set it up&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a group for each brand (or set of brands) your support team owns. For example, &lt;code&gt;Brand A Users&lt;/code&gt;, &lt;code&gt;Brand B Users&lt;/code&gt;, &lt;code&gt;VIP Fans&lt;/code&gt;, and so on.&lt;/li&gt;
&lt;li&gt;Assign a &lt;strong&gt;Help Desk Admin&lt;/strong&gt; (or equivalent custom role) at the &lt;code&gt;Brand A Users&lt;/code&gt; Group level and so on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Brand Help Desk access via group roles — Help Desk A can manage Alex through a group-scoped role; Help Desk B has no assignment and cannot&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Brand A help desk can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reset passwords&lt;/li&gt;
&lt;li&gt;Manage MFA devices&lt;/li&gt;
&lt;li&gt;Perform recovery actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;only&lt;/strong&gt; for users in the Brand A group — even if those users also belong to other brands or global groups.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This lets you keep a single, clean user identity while still giving each support team exactly the access they need.&lt;/p&gt;
&lt;h3&gt;2. Specialized admin teams&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You have a small, expert team that handles high-risk operations, such as managing MFA devices for executives or handling escalated recovery flows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to set it up&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create groups like &lt;code&gt;Executives&lt;/code&gt;, &lt;code&gt;High-Risk Accounts&lt;/code&gt;, or &lt;code&gt;Escalated Support&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build a narrow custom role focused on the operations this team is allowed to perform (for example, &lt;strong&gt;MFA device management&lt;/strong&gt; and &lt;strong&gt;account recovery&lt;/strong&gt; only).&lt;/li&gt;
&lt;li&gt;Assign that custom role at the high-risk group level.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The specialized team can perform sensitive operations only for the users you’ve explicitly grouped, without granting those permissions over the entire population or environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Designing your model with group-level assignments&lt;/h2&gt;
&lt;p&gt;When you’re planning how to use group-level assignments, a few guiding questions help:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How do I delegate based on my organizational structure?&lt;/strong&gt;
Think of your organizational structure and goals first, then align administration to it using groups and populations for finer delegation and environment or organization for those needing full visibility and control.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What’s the “home” for my admins?&lt;/strong&gt;
Generally continue to use the administrators environment as the home for admin identities, and then assign roles over the right mix of environments, populations, and groups from there. If your delegated admins are also end users, consider keeping them in the production environment rather than separate. Think about how you want to separate or combine the administrative responsibilities for a person from their end user identity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The end result is an administration model that reflects how your teams actually work day to day, while still keeping a single, consistent identity for every user.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.5uUWCk_D_ZlpQmJ.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Securing ChatGPT apps with OAuth 2.0 and CIBA</title><link>https://developer.pingidentity.com/blog/securing-chatgpt-apps/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/securing-chatgpt-apps/</guid><description>How to secure a ChatGPT app with Ping Identity, OAuth, PingOne Authorize, OAuth Token Exchange, and CIBA.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://carbonefederico.github.io/blog-content/posts/securing-chatgpt-apps/&quot;&gt;Federico Carbone’s blog site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;Enterprises are now building apps and tools for personal agents, and those agents rarely operate in a single trust model. Some capabilities need access to public or low-friction resources. Others need the agent to act on behalf of a signed-in user to reach private data or protected APIs. Some actions go further and require explicit user approval before the agent can complete them.&lt;/p&gt;
&lt;p&gt;In this blog, I describe how the PingOne platform can be used to secure a personal agent through a demo ChatGPT hotel booking app that uses OAuth 2.0, PingOne Authorize, Token Exchange, and CIBA to move from public access to authenticated access and then to transaction approval.&lt;/p&gt;
&lt;h2&gt;MyHotels demo&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MyHotels&lt;/code&gt; is a demo ChatGPT app built with the &lt;a href=&quot;https://developers.openai.com/apps-sdk&quot;&gt;OpenAI Apps SDK&lt;/a&gt;. It lets a user search for hotels, ask for member-only pricing, and start a booking that requires end-user approval before it completes. I use this demo application to show how Ping Identity can protect a ChatGPT personal agent when that agent needs to move across different trust levels.&lt;/p&gt;
&lt;p&gt;The following diagram shows the high-level logical architecture for the demo.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;%%{init: {&apos;flowchart&apos;: {&apos;curve&apos;: &apos;linear&apos;}}}%%&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;flowchart&lt;/span&gt;&lt;span&gt; LR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    U&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;User&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt; --&amp;gt;&lt;/span&gt;&lt;span&gt; CG&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    U &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; P1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    subgraph&lt;/span&gt;&lt;span&gt; CG&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;ChatGPT Host Runtime&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        direction TB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        CORE&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;ChatGPT&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        WR&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Widget Runtime\n(runs Widget UI)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    subgraph&lt;/span&gt;&lt;span&gt; MCP&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;MCP Server&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        direction TB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        TOOLS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Tools&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        RES&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Resources\n(publishes Widget UI)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    API&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Backend API&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P1&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;PingOne&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CORE &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; TOOLS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CORE &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; RES&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CORE &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; WR&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CORE &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; P1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; API&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP &lt;/span&gt;&lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt; P1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;MyHotels logical architecture&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User: interacts with ChatGPT and the mounted widget to search hotels, request member rates, and approve or finalize booking actions. The user also signs in and completes approval prompts through PingOne.&lt;/li&gt;
&lt;li&gt;ChatGPT Host Runtime: discovers MCP tools, resources, and authentication requirements; calls MCP tools on behalf of the user; reads widget resources; mounts the widget in its sandboxed runtime; and uses PingOne during the OAuth flow for protected MCP tools.&lt;/li&gt;
&lt;li&gt;Widget Runtime: runs the MyHotels widget UI inside ChatGPT and calls tools back through ChatGPT.&lt;/li&gt;
&lt;li&gt;MCP Server: exposes the hotel tools, publishes the widget HTML as an MCP resource, calls PingOne Authorize for policy decisions, exchanges tokens for backend API access, starts CIBA approval when policy requires end-user consent, and calls the backend hotel API.&lt;/li&gt;
&lt;li&gt;Backend API: owns the demo hotel business surface, including the hotel catalog, member-rate access, booking quotes, booking intents, and mocked booking confirmation.&lt;/li&gt;
&lt;li&gt;PingOne: provides the identity and authorization services used by the demo, including agent registration, user authentication, token exchange, policy decisions, and CIBA-based end-user approval.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;End user experience&lt;/h2&gt;
&lt;p&gt;The following videos demonstrate the end-user experience. First, the no-session journey shows what happens when the user starts from a fresh ChatGPT conversation and crosses from a public flow into a protected one. Second, the existing-session journey shows the same general flow when the user already has an authenticated session and can move more quickly into protected actions:&lt;/p&gt;
&lt;p&gt;For this demo, payment details are assumed to already be on file with &lt;code&gt;MyHotels&lt;/code&gt;. ChatGPT does not collect or submit card data. After the required authorization checks are satisfied, the backend booking service is the component that completes the mocked payment and confirms the booking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No-session journey:&lt;/strong&gt;&lt;/p&gt;

  
  Your browser does not support the video tag.

&lt;p&gt;&lt;strong&gt;Existing-session journey:&lt;/strong&gt;&lt;/p&gt;

  
  Your browser does not support the video tag.

&lt;p&gt;The following video shows the booking flow when the policy uses transaction thresholds: bookings below &lt;code&gt;200 EUR&lt;/code&gt; proceed without additional approval, bookings above &lt;code&gt;200 EUR&lt;/code&gt; require user approval, and bookings above &lt;code&gt;1000 EUR&lt;/code&gt; are denied.&lt;/p&gt;

  
  Your browser does not support the video tag.

&lt;h2&gt;How ChatGPT apps work&lt;/h2&gt;
&lt;p&gt;At a high-level, ChatGPT is the host runtime, and the demo app exposes capabilities through MCP tools and resources. When ChatGPT decides to call a tool such as hotel search, it sends the tool request to the MCP server. When a tool response includes an output template, ChatGPT reads the matching resource and mounts the widget UI inside its own runtime.&lt;/p&gt;
&lt;p&gt;The tool surface is small and intentional:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search_hotels&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search_hotels_member_rates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prepare_booking&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;finalize_booking&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The widget is delivered as the MCP resource &lt;code&gt;ui://widget/myhotels-widget.html&lt;/code&gt;. The widget is not just a public web page that ChatGPT opens. It is a resource served through MCP and mounted by ChatGPT when a tool response references it through &lt;code&gt;openai/outputTemplate&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After mounting, the widget uses the &lt;code&gt;window.openai&lt;/code&gt; bridge to interact with ChatGPT. In this project, that bridge is used in two different ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sendFollowUpMessage(...)&lt;/code&gt; to ask ChatGPT to call the protected member-rates tool&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callTool(...)&lt;/code&gt; to invoke booking-related tools directly from the widget&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gives the app a useful split:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ChatGPT remains the orchestrator&lt;/li&gt;
&lt;li&gt;the widget becomes the interactive UI surface&lt;/li&gt;
&lt;li&gt;the backend systems stay behind the MCP boundary&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Identity Provider&lt;/h2&gt;
&lt;p&gt;At a high-level, Ping Identity plays four different roles in this demo. First, PingOne supports user authentication for ChatGPT when the app crosses into protected functionality. Second, the MCP server uses PingOne Authorize to evaluate each tool call against policy. Third, the MCP server uses OAuth token exchange to obtain a backend API token with the correct audience and scope. Fourth, PingOne supports the CIBA approval flow used by the MCP server for booking approval:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PingOne handles user sign-in&lt;/li&gt;
&lt;li&gt;PingOne Authorize decides whether the tool call should be allowed&lt;/li&gt;
&lt;li&gt;token exchange gives the backend API a token minted for its own resource&lt;/li&gt;
&lt;li&gt;CIBA handles higher-friction approval when policy requires it&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;PingOne Authorize&lt;/h2&gt;
&lt;p&gt;The current demo does not rely only on OAuth scopes at the MCP layer. Every tool call is sent to PingOne Authorize with a payload that identifies the MCP service, the requested tool, selected request parameters, and the inbound ChatGPT bearer token, so that PingOne Authorize can decide whether the tool call should be allowed.&lt;/p&gt;
&lt;p&gt;That lets the MCP server enforce a mixed model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search_hotels&lt;/code&gt; remains available to unauthenticated users so the public hotel catalog can be searched without sign-in&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search_hotels_member_rates&lt;/code&gt; is allowed only when the request is initiated by a user who is allowed to access ChatGPT, the actor context shows the ChatGPT connector acting on behalf of that user, the token is intended for the MCP resource, and the token carries the correct scope&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prepare_booking&lt;/code&gt; is allowed only when the same user, actor, audience, and MCP booking-scope checks pass, and the decision also evaluates quote attributes such as &lt;code&gt;totalPrice&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;finalize_booking&lt;/code&gt; is allowed only when the same protected-user, actor, audience, and scope checks pass&lt;/li&gt;
&lt;li&gt;any MCP request that does not satisfy one of those allowed patterns is denied&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For booking, the important point is that policy is not binary. The quote amount changes the outcome:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;below &lt;code&gt;200 EUR&lt;/code&gt;: permit&lt;/li&gt;
&lt;li&gt;above &lt;code&gt;200 EUR&lt;/code&gt;: permit with a human-in-the-loop obligation&lt;/li&gt;
&lt;li&gt;above &lt;code&gt;1000 EUR&lt;/code&gt;: deny&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So in the current design, PingOne Authorize decides whether booking is allowed immediately, requires explicit user approval, or should be blocked entirely.&lt;/p&gt;
&lt;h2&gt;Token exchange&lt;/h2&gt;
&lt;p&gt;The ChatGPT-facing token is meant for the MCP surface. The backend API is a different protected resource with its own audience and scopes. Instead of forwarding the original token and overloading its meaning, the MCP server exchanges it for a backend API token.&lt;/p&gt;
&lt;p&gt;This gives us clearer boundaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ChatGPT gets a token for the MCP resource&lt;/li&gt;
&lt;li&gt;the MCP server gets a token for the backend API resource&lt;/li&gt;
&lt;li&gt;the backend validates a token that was actually minted for it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the current configuration, the ChatGPT-facing MCP token can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;myhotels-hotelmcp&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;given_name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;groups&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;ChatGPT User&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;chatgpt-client-id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this demo, the exchanged backend token can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;https://auth.pingone.../as&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;myhotels-hotelapi&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;user-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;my-hotels:api:member_rates&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;client_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;mcp-token-exchange-client-id&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;mcp-token-exchange-client-id&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;act&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;chatgpt-client-id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are two important things in these tokens.&lt;/p&gt;
&lt;p&gt;First, the token audiences and scopes stay separated. The ChatGPT-facing token is for the MCP resource. The exchanged token is for the backend API and carries one of the backend API scopes such as &lt;code&gt;my-hotels:api:member_rates&lt;/code&gt; or &lt;code&gt;my-hotels:api:book&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Second, the token preserves an actor chain. The first token already identifies the ChatGPT client as the acting party. After exchange, the subject is still the user, &lt;code&gt;user-123&lt;/code&gt;, but the token now also shows that the MCP token-exchange client acted on behalf of the original ChatGPT client. That is exactly the kind of traceability we want when a personal agent triggers downstream API calls through an intermediary service.&lt;/p&gt;
&lt;h2&gt;CIBA&lt;/h2&gt;
&lt;p&gt;In many architectures, the client is the component that initiates CIBA. In this demo, we moved it to the MCP server rather than the ChatGPT client. We still wanted approval to be bound to a concrete business transaction, but we do not control the ChatGPT runtime like a conventional first-party client. The MCP layer is therefore the practical control point: it has the user context, it receives the policy obligation from PingOne Authorize, it can initiate approval with PingOne, and it can coordinate the protected call path to the backend API.&lt;/p&gt;
&lt;p&gt;The backend still owns the booking intent and its lifecycle. The MCP server starts CIBA, stores the &lt;code&gt;auth_req_id&lt;/code&gt;, and maps it to the server-side transaction ID returned by the backend. The widget only knows the transaction ID and polls for status updates.&lt;/p&gt;
&lt;h2&gt;The user journeys in detail&lt;/h2&gt;
&lt;h3&gt;1. Public hotel search&lt;/h3&gt;
&lt;p&gt;The simplest journey starts with a natural-language request such as “show me hotels in Milan”.&lt;/p&gt;
&lt;p&gt;ChatGPT calls &lt;code&gt;search_hotels&lt;/code&gt; on the MCP server. The MCP server first asks PingOne Authorize for a policy decision. If the decision is &lt;code&gt;PERMIT&lt;/code&gt;, it returns hotel results and the widget template reference. ChatGPT then reads &lt;code&gt;ui://widget/myhotels-widget.html&lt;/code&gt;, mounts the widget, and passes the tool output into the widget runtime.&lt;/p&gt;
&lt;p&gt;The widget then renders:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a map&lt;/li&gt;
&lt;li&gt;hotel markers&lt;/li&gt;
&lt;li&gt;hotel cards&lt;/li&gt;
&lt;li&gt;standard nightly rates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No authentication is required for this part of the experience.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; U&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; CG&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; ChatGPT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; MCP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; MCP Server&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; PDP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; PingOne Authorize&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; W&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Widget&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    U&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Search hotels in a city&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tools/call(search_hotels)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;PDP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PDP decision(search_hotels)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PDP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PERMIT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; hotels + outputTemplate&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; resources/read(ui://widget/myhotels-widget.html)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; widget HTML resource&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; instantiate widget runtime&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; provide toolOutput / globals&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; render hotels and map&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Public hotel search flow&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;2. Member-rate search&lt;/h3&gt;
&lt;p&gt;The second journey starts from the rendered widget. The user clicks &lt;code&gt;Show Member Rates&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The widget does not fetch pricing directly from the backend. Instead, it asks ChatGPT to call &lt;code&gt;search_hotels_member_rates&lt;/code&gt;. That tool is protected. If the user is not signed in or does not have the required scope, the MCP server returns an OAuth challenge. ChatGPT handles the sign-in flow and retries the tool call with a valid bearer token.&lt;/p&gt;
&lt;p&gt;After that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the MCP server asks PingOne Authorize for a decision using the inbound bearer token&lt;/li&gt;
&lt;li&gt;PingOne Authorize verifies that the caller is a ChatGPT user and that the MCP token has the required scope&lt;/li&gt;
&lt;li&gt;it performs token exchange to obtain a backend API token&lt;/li&gt;
&lt;li&gt;it calls the backend API with the exchanged token&lt;/li&gt;
&lt;li&gt;it returns hotel results with member pricing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The widget updates in place and shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;discounted member rates&lt;/li&gt;
&lt;li&gt;savings compared to the standard rate&lt;/li&gt;
&lt;li&gt;the authenticated user display name in the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a good example of progressive authentication. The user can explore first and authenticate only when the value of signing in is obvious.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; U&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; W&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Widget&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; CG&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; ChatGPT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; MCP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; MCP Server&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; P1&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Ping Identity&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; PDP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; PingOne Authorize&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; API&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Backend API&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    U&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Show member rates&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; window.openai.sendFollowUpMessage(...)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tools/call(search_hotels_member_rates)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;PDP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PDP decision(search_hotels_member_rates)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    alt&lt;/span&gt;&lt;span&gt; token missing or insufficient&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; OAuth challenge&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; OAuth flow&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; access token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; retry tools/call(search_hotels_member_rates)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;PDP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PDP decision(search_hotels_member_rates)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PDP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PERMIT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; token exchange&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; backend API token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; GET /hotels?memberRates=true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    API&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; member-rate hotels&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; member-rate hotels&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; provide updated toolOutput&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Member-rate search flow with OAuth and token exchange&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;3. Booking with approval&lt;/h3&gt;
&lt;p&gt;The booking journey starts when the user clicks &lt;code&gt;Book&lt;/code&gt; on a hotel card and submits check-in date and number of nights.&lt;/p&gt;
&lt;p&gt;The widget calls &lt;code&gt;prepare_booking&lt;/code&gt;. That tool is also protected, but the logic is now more nuanced than a simple authenticated-or-not check.&lt;/p&gt;
&lt;p&gt;The MCP first exchanges the token for a backend API token and asks the backend for an authoritative booking quote. It then sends the inbound bearer token, the tool name, and the quote attributes such as &lt;code&gt;totalPrice&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt; to PingOne Authorize. If policy returns &lt;code&gt;PERMIT&lt;/code&gt;, MCP creates the booking intent. If policy also returns a human-in-the-loop obligation, MCP starts CIBA. If no such obligation is returned, MCP can confirm the booking immediately.&lt;/p&gt;
&lt;p&gt;This is a useful design choice because the approval threshold is policy-driven rather than hard-coded into the app.&lt;/p&gt;
&lt;p&gt;Across the MCP and backend layers, the system stores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the transaction ID&lt;/li&gt;
&lt;li&gt;the booking owner&lt;/li&gt;
&lt;li&gt;the CIBA &lt;code&gt;auth_req_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the quote and approval state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The widget receives either an immediate confirmation or a pending booking status. For approval-driven bookings, it starts polling &lt;code&gt;finalize_booking&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;From there the flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The MCP server asks PingOne Authorize for each finalization attempt.&lt;/li&gt;
&lt;li&gt;It verifies that the caller still matches the booking owner.&lt;/li&gt;
&lt;li&gt;It polls PingOne using the stored &lt;code&gt;auth_req_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When approval reaches &lt;code&gt;approved&lt;/code&gt;, the MCP server exchanges the token again if needed and confirms the backend booking intent.&lt;/li&gt;
&lt;li&gt;The widget reflects the current state in ChatGPT.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the user interaction is conversational at the start, visual in the middle, and approval-driven at the end.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; U&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; W&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Widget&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; CG&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; ChatGPT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; MCP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; MCP Server&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; PDP&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; PingOne Authorize&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; API&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Backend API&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    participant&lt;/span&gt;&lt;span&gt; P1&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; Ping Identity&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    U&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Book hotel&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; window.openai.callTool(&quot;prepare_booking&quot;, ...)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tools/call(prepare_booking)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; parse token sub for demo owner state&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; token exchange&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; backend API token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST /booking-quotes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    API&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; quote with nightlyRate + totalPrice&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;PDP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PDP decision(prepare_booking with quote)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    PDP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PERMIT, optional USER-AZ-REQUIRED&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST /booking-intents&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    API&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingIntent pending&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    alt&lt;/span&gt;&lt;span&gt; USER-AZ-REQUIRED returned&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST cibaAuthorization&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; auth_req_id + expires_in + interval&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingApproval pending&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; provide updated tool result&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        loop&lt;/span&gt;&lt;span&gt; while pending&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            W&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; window.openai.callTool(&quot;finalize_booking&quot;, ...)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tools/call(finalize_booking)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;PDP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PDP decision(finalize_booking)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            PDP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; PERMIT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; parse token sub and verify ownerSub matches&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST token (CIBA poll)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; pending or final state&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            opt&lt;/span&gt;&lt;span&gt; approved&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;P1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; token exchange&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                P1&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; backend API token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST /booking-intents/:transactionId/confirm&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                API&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingIntent confirmed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingApproval update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; provide updated tool result&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    else&lt;/span&gt;&lt;span&gt; no human-in-the-loop obligation&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; POST /booking-intents/:transactionId/confirm&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        API&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingIntent confirmed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        MCP&lt;/span&gt;&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;CG&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; bookingApproval approved&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        CG&lt;/span&gt;&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;W&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; provide updated tool result&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Booking flow with CIBA-based approval&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MyHotels&lt;/code&gt; is a compact but realistic example of how Ping Identity can secure a ChatGPT personal agent across different trust boundaries. In this demo, PingOne provides the main identity capabilities that make the flow work safely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user authentication when the agent crosses into protected functionality&lt;/li&gt;
&lt;li&gt;centralized MCP authorization through PingOne Authorize&lt;/li&gt;
&lt;li&gt;audience and scope-specific token issuance for the MCP layer&lt;/li&gt;
&lt;li&gt;OAuth 2.0 Token Exchange so the backend API receives a token minted for its own protected resource&lt;/li&gt;
&lt;li&gt;actor-chain visibility that preserves how the request moved from ChatGPT to MCP to the backend&lt;/li&gt;
&lt;li&gt;CIBA-based approval for higher-risk actions such as booking&lt;/li&gt;
&lt;li&gt;policy-based thresholds that can allow, step up, or deny booking actions based on amount&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Taken together, those capabilities let the personal agent move from public access to authenticated access and then to explicit transaction approval without collapsing everything into one token or one client-side flow.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Demo app: &lt;a href=&quot;https://github.com/carbonefederico/ai-myhotels-chatgpt-app&quot;&gt;ai-myhotels-chatgpt-app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OAuth 2.0 Token Exchange spec: &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8693&quot;&gt;RFC 8693&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CIBA spec: &lt;a href=&quot;https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html&quot;&gt;OpenID Client-Initiated Backchannel Authentication (CIBA) Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Apps SDK: &lt;a href=&quot;https://developers.openai.com/apps-sdk&quot;&gt;OpenAI Apps SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.BpDpkEdj_2uRV7X.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Help us shape the future of Ping Identity&apos;s documentation</title><link>https://developer.pingidentity.com/blog/help-us-shape-the-future-of-ping-identity-documentation/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/help-us-shape-the-future-of-ping-identity-documentation/</guid><description>Take the 2026 Annual Documentation Feedback Survey and help Ping Identity prioritize the improvements that matter most to developers and technical implementers.</description><pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you use Ping Identity documentation to build, configure, troubleshoot, or learn, we’d love your feedback. Our 2026 Annual Documentation Feedback Survey takes 5 – 10 minutes to complete and is designed to help us prioritize improvements that will make the biggest difference for you.&lt;/p&gt;
&lt;p&gt;This year’s survey asks about the full documentation experience: how easy it is to find content, whether information is easy to apply, how well the docs help you resolve issues, and whether they help you discover new ways to use the products.&lt;/p&gt;
&lt;p&gt;It also asks what kinds of content you want more of, including developer content such as code samples, example apps, and SDK or API docs. In addition, we’re asking how you use AI tools to find Ping Identity information and what you want those tools to do better for you.&lt;/p&gt;
&lt;p&gt;For developers and technical implementers, that matters. Good documentation should help you get started faster, find the right configuration details, connect information across pages, and turn examples into something you can actually use in your environment. Your feedback helps us see where we’re doing that well and where we still need to improve.&lt;/p&gt;
&lt;p&gt;If you’re willing to go deeper, the survey also includes an option to volunteer for a 30-minute follow-up interview or other research activities. That gives us a chance to understand the story behind the scores and comments, not just the numbers.&lt;/p&gt;
&lt;h2&gt;Take the survey&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://surveys.hotjar.com/97d3b479-91fd-4225-ae6c-6b0d2848d19a&quot;&gt;Take the Annual Documentation Feedback Survey&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks in advance for helping us improve the documentation experience for builders, admins, and everyone else who depends on Ping documentation to get work done.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CmlKSW6q_mnqbE.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Docs for Agents: making Ping Identity documentation work with AI</title><link>https://developer.pingidentity.com/blog/docs-for-agents/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/docs-for-agents/</guid><description>How we made our docs easy for AI agents to discover and consume.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Developers increasingly build with AI coding agents, and one of the first things those agents do when working on an identity integration is try to pull in your docs. If your documentation isn’t structured for that, the agent either gets noisy HTML it has to parse, or it gives up and hallucinates the API surface instead.&lt;/p&gt;
&lt;p&gt;There are ways to make your docs more agent-friendly, and we recently implemented a number of these solutions for Ping Identity’s documentation. The result: our docs are now much easier for agents to discover and consume, which means better accuracy and a smoother experience for developers building with AI.&lt;/p&gt;
&lt;h2&gt;The problem: great docs, invisible to agents&lt;/h2&gt;
&lt;p&gt;Ping Identity has extensive technical documentation — thousands of pages across &lt;a href=&quot;https://docs.pingidentity.com&quot;&gt;docs.pingidentity.com&lt;/a&gt; and &lt;a href=&quot;https://developer.pingidentity.com&quot;&gt;developer.pingidentity.com&lt;/a&gt;. But “extensive” doesn’t automatically mean “agent-friendly.”&lt;/p&gt;
&lt;p&gt;Before this work, an AI agent trying to use our docs would hit a few walls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HTML noise&lt;/strong&gt;: Documentation pages are built for browsers. Navigation chrome, scripts, and hundreds of lines of HTML overhead are irrelevant for an agent — and they’re expensive in terms of tokens. Stripping HTML to get to the actual content is possible, but imperfect, since the agent has to guess which parts are content and which parts are noise.&lt;/p&gt;
&lt;p&gt;Well-structured, semantic HTML does carry useful context, but in practice most HTML pages aren’t very well-structured or semantically rich — and even when they are, the signal-to-noise ratio rarely justifies the token cost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No discovery mechanism&lt;/strong&gt;: Even if an agent knew our docs existed, there was no structured way for it to find the right page. It would have to either crawl the entire site or guess at URLs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No machine-readable product metadata&lt;/strong&gt;: Pages described products in natural language, but nothing told a search engine or agent in structured terms &lt;em&gt;which&lt;/em&gt; product a page was about.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On top of all this, AI-driven traffic already accounts for a significant percentage of site visits. We were already being crawled; we just weren’t giving those crawlers what they needed.&lt;/p&gt;
&lt;p&gt;To make matters more complex, there isn’t really a single “right” way to solve these problems. Best practices and specifications are still evolving, and it may be a while before an approach that works across most agents solidifies into a standard. Any solution we implemented had to be flexible enough to evolve as the ecosystem does, while still providing immediate value to developers building with AI today.&lt;/p&gt;
&lt;h2&gt;What we built&lt;/h2&gt;
&lt;p&gt;Because of the evolving landscape, we took a multipronged approach, implementing several complementary features to make our docs more agent-friendly. Our goal was to make it easy for agents to discover the docs they need, give them a clean, efficient way to fetch the content, and provide structured metadata to help them understand what they’re looking at, all without diminishing the experience for human readers.&lt;/p&gt;
&lt;h3&gt;Markdown alternates for every page&lt;/h3&gt;
&lt;p&gt;The most significant single change: every documentation page now has a Markdown alternate at the same URL, with &lt;code&gt;.html&lt;/code&gt; swapped for &lt;code&gt;.md&lt;/code&gt;, which lives parallel to the HTML version. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;https://docs.pingidentity.com/pingam/8/setup/am-admin-interface-tools.html&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;→&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;https://docs.pingidentity.com/pingam/8/setup/am-admin-interface-tools.md&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Markdown alternates strip out navigation, scripts, and all the browser-facing overhead, leaving just the content. Pulling Markdown instead of HTML can eliminate a significant portion of the data an agent would otherwise have to process as noise. That’s a dramatic drop in token cost and a meaningful improvement in accuracy. Taking a random sampling of pages across our docsets, the Markdown version is typically between 85% and 95% smaller than the HTML version, while still retaining the content an AI agent actually cares about.&lt;/p&gt;
&lt;p&gt;To improve discovery of the Markdown alternates, we also added a &lt;code&gt;&amp;lt;link rel=&quot;alternate&quot; type=&quot;text/markdown&quot;&amp;gt;&lt;/code&gt; element in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of every HTML page, pointing to the corresponding Markdown URL. This way, an agent that starts with the HTML page can easily find the Markdown version without having to guess the URL structure. To help humans discover it too, we also added a link to the Markdown version at the top of each page.&lt;/p&gt;
&lt;p&gt;We generate the markdown versions based on the published HTML, rather than just exposing our raw files. This is because we write our documentation using AsciiDoc. While most agents are perfectly happy to handle AsciiDoc, we do enough post processing during our build pipeline that it wasn’t feasible to just expose the raw AsciiDoc, and if we were going to have to process the files anyway, we may as well align with what has become an unofficial standard for AI-oriented docs. The Markdown generation happens at build time via an extension to our Antora build pipeline. Building the Markdown and the HTML from the same source content keeps them in sync — you can’t accidentally publish docs that have one without the other.&lt;/p&gt;
&lt;h3&gt;llms.txt indexes&lt;/h3&gt;
&lt;p&gt;Markdown alternates solve &lt;em&gt;fetching&lt;/em&gt; a known page. But how does an agent know which pages exist?&lt;/p&gt;
&lt;p&gt;The answer is &lt;code&gt;llms.txt&lt;/code&gt;, an &lt;a href=&quot;https://llmstxt.org&quot;&gt;emerging standard&lt;/a&gt; for giving AI models a curated, structured index of a site — similar in spirit to &lt;code&gt;robots.txt&lt;/code&gt;, but designed for context consumption rather than crawling control.&lt;/p&gt;
&lt;p&gt;We generate &lt;code&gt;llms.txt&lt;/code&gt; files as part of the same Markdown build pipeline, at two levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Per-docset&lt;/strong&gt;: &lt;code&gt;https://docs.pingidentity.com/pingam/llms.txt&lt;/code&gt; lists every page in the PingAM docset with its title and Markdown URL. An agent working on a PingAM integration can load this file to get a full inventory of available content, then selectively fetch the pages relevant to its task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site-wide&lt;/strong&gt;: &lt;code&gt;https://docs.pingidentity.com/llms.txt&lt;/code&gt; and &lt;code&gt;https://developer.pingidentity.com/llms.txt&lt;/code&gt; aggregate the per-docset files into a single index organized by product.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JSON-LD structured data&lt;/h3&gt;
&lt;p&gt;In parallel with the Markdown and &lt;code&gt;llms.txt&lt;/code&gt; work, we added &lt;a href=&quot;https://json-ld.org&quot;&gt;JSON-LD&lt;/a&gt; structured data to every documentation page. JSON-LD is a machine-readable format for expressing things like “this page is a TechArticle about PingAM, published on this date, by Ping Identity.”&lt;/p&gt;
&lt;p&gt;Search engines and AI knowledge graphs already know how to consume this. Adding it to our pages helps both SEO and agent context — an agent parsing a docs page can read the JSON-LD block and immediately understand the product, product version, and content type rather than having to infer it from the natural language text. For a first pass, establishing the product/content-type relationship cleanly was the highest-value thing to ship, with room to iterate as needs evolve.&lt;/p&gt;
&lt;h3&gt;A “Docs for Agents” landing page&lt;/h3&gt;
&lt;p&gt;To tie all of this together for humans and agents alike, we published a &lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/docs-for-agents.html&quot;&gt;Docs for Agents&lt;/a&gt; page explaining how everything fits together: where the Markdown alternates are, how the &lt;code&gt;llms.txt&lt;/code&gt; files are structured, and recommended patterns for loading focused context on a specific product vs. discovering what’s available across the full site.&lt;/p&gt;
&lt;p&gt;This isn’t necessarily a standard, but it’s a useful resource, and one that others in the industry have also opted to do (Cloudflare has a &lt;a href=&quot;https://developers.cloudflare.com/docs-for-agents/&quot;&gt;similar page&lt;/a&gt;, for instance). It’s a way to signal to developers that “yes, we know you’re using AI agents, and yes, we have features in place to support that.”&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Accept: text/markdown&lt;/code&gt; content negotiation&lt;/h3&gt;
&lt;p&gt;Some agents send an &lt;code&gt;Accept: text/markdown&lt;/code&gt; header with their requests, expecting the server to return Markdown if it’s available. We’ve recently added support for this, including a fallback to the HTML version if the Markdown version isn’t available for some reason.&lt;/p&gt;
&lt;p&gt;This approach is gaining a lot of traction as a more elegant way to handle Markdown alternates, since it doesn’t require the agent to manage two separate URLs for each page. The agent can just request the normal URL and let content negotiation handle the rest. It’s still early days for this pattern, and not all agents support it yet, but we wanted to be ready for it as it becomes more common. You can learn more about this approach at &lt;a href=&quot;https://acceptmarkdown.com&quot;&gt;Accept: text/markdown&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Try it out&lt;/h2&gt;
&lt;p&gt;If you’re building an integration and want to give your AI coding agent focused context on a Ping product:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Point the agent at the product’s &lt;code&gt;llms.txt&lt;/code&gt;&lt;/strong&gt;: For example, &lt;code&gt;https://docs.pingidentity.com/pingam/llms.txt&lt;/code&gt; for PingAM. The agent can scan the titles and selectively fetch the pages most relevant to what you’re building.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fetch Markdown instead of HTML&lt;/strong&gt;: Swap &lt;code&gt;.html&lt;/code&gt; for &lt;code&gt;.md&lt;/code&gt; on any page URL, or extract the &lt;code&gt;&amp;lt;link rel=&quot;alternate&quot; type=&quot;text/markdown&quot;&amp;gt;&lt;/code&gt; URL from the page &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. If your agent supports it, you can also just send an &lt;code&gt;Accept: text/markdown&lt;/code&gt; header and let content negotiation do the work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not sure which product docset you need?&lt;/strong&gt; Start at &lt;code&gt;https://docs.pingidentity.com/llms.txt&lt;/code&gt; or &lt;code&gt;https://developer.pingidentity.com/llms.txt&lt;/code&gt; for an index of everything available.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What’s next&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Enriched page descriptions for &lt;code&gt;llms.txt&lt;/code&gt; entries&lt;/strong&gt;: Right now each entry in an &lt;code&gt;llms.txt&lt;/code&gt; file is just the page title and URL. The spec recommends a short description (≤150 characters) alongside each link. We’re updating all of our docs pages to include these descriptions, which should help make the &lt;code&gt;llms.txt&lt;/code&gt; indexes even more useful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docs MCP server&lt;/strong&gt;: The longer-term vision is a Docs MCP server that exposes our documentation as callable tools rather than just static files. An agent using MCP would be able to search across products, retrieve specific sections, and get structured answers without having to manage the &lt;code&gt;llms.txt&lt;/code&gt;/Markdown workflow manually. This is in active development and should be launching soon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring and iteration&lt;/strong&gt;: We’re actively monitoring how agents are using these features and gathering feedback from developers. The AI ecosystem is evolving rapidly, so we’re committed to iterating on our approach as best practices and standards emerge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Specifications&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://acceptmarkdown.com&quot;&gt;Accept: text/markdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agentdocsspec.com&quot;&gt;Agent-Friendly Documentation Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://json-ld.org&quot;&gt;JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://llmstxt.org&quot;&gt;llms.txt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;“Docs for Agents” Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/docs-for-agents.html&quot;&gt;Ping Identity’s Docs for Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/docs-for-agents/&quot;&gt;Cloudflare’s Docs for Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.D2vzeIl4_ZIP955.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Securing the PingOne DaVinci Widget with the Backend-for-Frontend (BFF) Pattern</title><link>https://developer.pingidentity.com/blog/securing-the-pingone-davinci-widget-with-the-backend-for-frontend-bff-pattern/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/securing-the-pingone-davinci-widget-with-the-backend-for-frontend-bff-pattern/</guid><description>A walkthrough of how to embed the DaVinci widget in an SPA while keeping OAuth tokens and DaVinci API credentials safely on a Node.js BFF backend.</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Web application architecture has gone through several major evolutions over the last 30 years - and I’m old enough to have lived through most of them. Thankfully, we’ve largely moved on from the era of server-rendered UI frameworks like Java Servlets, PHP, and ASP applications that rebuilt an entire page on every interaction. In their place, single page applications (SPAs) have become the dominant model for modern web experiences.&lt;/p&gt;
&lt;p&gt;SPAs dramatically improve responsiveness and usability by moving much of the application control logic into the browser, where JavaScript can interact directly with backend APIs without requiring constant full-page refreshes. From a user experience perspective, the benefits are undeniable.&lt;/p&gt;
&lt;p&gt;For identity architects and security practitioners, however, this shift introduced a new set of challenges. Traditional authentication and session-management patterns were designed around trusted server-side applications. As applications moved toward increasingly “pure” SPA architectures, those patterns became harder to apply safely and consistently.&lt;/p&gt;
&lt;p&gt;In my opinion, the challenge of building a truly secure browser-only SPA remains largely unsolved. While modern OAuth and browser security standards have improved significantly, exposing sensitive OAuth tokens directly to browser code still creates risks that many teams underestimate. That’s why I continue to believe that a secure backend following the Backend-for-Frontend (BFF) pattern is a critical component of any security-sensitive SPA architecture.&lt;/p&gt;
&lt;p&gt;In this article, we’ll walk through an approach for securing the PingOne DaVinci embedded widget using a BFF architecture that prevents sensitive OAuth tokens from ever being exposed to the browser client. The core idea is simple: keep the widget in the browser for the user experience, but keep the trust decisions, token handling, and application session establishment in the backend.&lt;/p&gt;
&lt;h2&gt;What is a widget anyway (and why would we use one?)&lt;/h2&gt;
&lt;p&gt;As a modern identity orchestration platform, DaVinci provides multiple integration models that allow applications to consume orchestration flows in the way that best matches their architecture and UX goals.&lt;/p&gt;
&lt;p&gt;For modern web applications, embedded authentication experiences have become increasingly popular. Rather than redirecting users to a hosted login page, authentication journeys are rendered directly within the application itself, creating a more seamless user experience. While there are certainly security tradeoffs worth discussing, the approach is now common enough that identity platforms need to support it well.&lt;/p&gt;
&lt;p&gt;DaVinci offers two primary approaches for embedded authentication experiences:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Headless integration using APIs and SDKs. Applications can use DaVinci APIs and SDKs to implement the entire authentication experience directly in client-side code. This gives development teams maximum control over UI and UX behavior, but also requires them to own and maintain the interaction logic themselves.&lt;/li&gt;
&lt;li&gt;The embedded DaVinci widget. The pre-built DaVinci widget provides a faster and more flexible way to embed authentication flows directly into a web application while allowing DaVinci to handle the orchestration and flow-rendering logic. The result is a seamless embedded experience with significantly less implementation effort.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this article, we’ll focus specifically on securing the embedded DaVinci widget integration itself rather than exploring every possible DaVinci deployment model. In particular, we’ll concentrate on two key security objectives. First, we’ll show how the widget can rely on a trusted backend service to securely initiate DaVinci flows without exposing sensitive DaVinci API credentials to browser-based JavaScript. Second, we’ll show how to complete the authentication process without returning OAuth tokens, DaVinci session tokens, or other sensitive handoff artifacts through the browser front channel.&lt;/p&gt;
&lt;p&gt;To demonstrate the approach, we’ll use a deliberately simple reference architecture consisting of two components. The frontend application is implemented as a lightweight HTML and JavaScript SPA that runs entirely in the browser and hosts the embedded DaVinci widget. Supporting this is a minimal backend service implemented in Node.js using the Express framework. By keeping the architecture intentionally straightforward, the example focuses on the core security concepts without introducing unnecessary framework complexity.&lt;/p&gt;
&lt;h2&gt;Why this pattern is worth the extra work&lt;/h2&gt;
&lt;p&gt;With an embedded authentication widget, there is an unavoidable architectural tension. The browser needs to host the user experience, because that is the whole point of embedding the widget. But the browser is not where we want to handle API credentials, OAuth tokens, refresh tokens, or durable authentication state.&lt;/p&gt;
&lt;p&gt;The pattern in this article draws a clean boundary. The widget still renders in the browser, but the BFF creates the authentication transaction, DaVinci binds the completed flow back to that transaction, and the tokens are delivered directly from DaVinci to the backend over a server-to-server callback. The browser is responsible for the interactive experience and for asking the BFF to finalize the local application session. It is not responsible for transporting token material.&lt;/p&gt;
&lt;p&gt;That is why this pattern works well. It preserves the embedded login experience, keeps OAuth tokens out of frontend JavaScript, avoids exposing DaVinci API credentials, gives the BFF a server-side transaction to validate, and lets the BFF regenerate the application session only after it has verified signed token claims that match the transaction it originally created.&lt;/p&gt;
&lt;p&gt;Full code for the sample implementation described below is available in &lt;a href=&quot;https://github.com/robotto-ping/acme-davinci-bff-public&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The code in this article is provided as a sample for educational purposes and is not intended for direct production use without review and adaptation for your specific environment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 1: Securely obtaining a DaVinci SDK token&lt;/h2&gt;
&lt;p&gt;The first challenge when embedding the DaVinci widget into a browser-based SPA is securely obtaining the SDK token required to initiate a flow. Normally, generating this token requires a DaVinci API key - a highly sensitive credential that should never be exposed to browser JavaScript or embedded into frontend source code.&lt;/p&gt;
&lt;p&gt;To avoid this, the browser never communicates directly with the DaVinci &lt;code&gt;/sdktoken&lt;/code&gt; endpoint. Instead, the frontend calls a trusted backend endpoint (&lt;code&gt;/dvtoken&lt;/code&gt;) implemented in our Express-based BFF service. The backend securely stores the DaVinci API key using environment variables and uses it to request a short-lived SDK token from the DaVinci orchestration API.&lt;/p&gt;
&lt;p&gt;In this pattern, &lt;code&gt;/dvtoken&lt;/code&gt; does one more important thing. Before requesting the SDK token, the BFF creates a short-lived authentication transaction. This transaction includes a &lt;code&gt;transactionID&lt;/code&gt;, a cryptographically strong nonce, and two time windows: one for starting the DaVinci flow and one for completing the overall authentication transaction.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; createAuthTransaction&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; issuedAt&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; Date.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    transactionID: crypto.&lt;/span&gt;&lt;span&gt;randomUUID&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    nonce: crypto.&lt;/span&gt;&lt;span&gt;randomBytes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;base64url&apos;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    issuedAt,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    startBy: issuedAt &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; AUTH_FLOW_START_TTL_MS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    completeBy: issuedAt &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; AUTH_TRANSACTION_TTL_MS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    status: &lt;/span&gt;&lt;span&gt;&apos;pending&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These two time windows are intentionally different. Starting the widget should happen quickly, so &lt;code&gt;startBy&lt;/code&gt; can be short. Completing the journey could take longer, especially if the flow includes MFA, identity verification, or any other user interaction that takes time, so &lt;code&gt;completeBy&lt;/code&gt; should be longer.&lt;/p&gt;
&lt;p&gt;The BFF stores this transaction server-side and also stores a copy in the pre-auth Express session associated with the browser. It then passes selected values into the DaVinci flow as SDK token parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  policyId: &lt;/span&gt;&lt;span&gt;WIDGET_POLICY_ID&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  parameters: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    transactionID: authTransaction.transactionID,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    nonce: authTransaction.nonce,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    expiresAt: authTransaction.startBy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser doesn’t need to know these values. They’re generated by the BFF and delivered directly to DaVinci as flow input parameters. The browser receives only the widget SDK token and the minimal configuration needed to render the widget.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	token: data.access_token,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	companyId: &lt;/span&gt;&lt;span&gt;COMPANY_ID&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	policyId: &lt;/span&gt;&lt;span&gt;WIDGET_POLICY_ID&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	apiRoot: &lt;/span&gt;&lt;span&gt;API_ROOT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Invoking the login flow with the widget&lt;/h2&gt;
&lt;p&gt;With the SDK token in hand, the frontend can initialize the DaVinci widget and invoke the login flow. The widget is configured to use &lt;code&gt;runFlow&lt;/code&gt;, along with the &lt;code&gt;apiRoot&lt;/code&gt;, &lt;code&gt;companyId&lt;/code&gt;, &lt;code&gt;policyId&lt;/code&gt;, and short-lived SDK token returned by the BFF. At this point, the browser has everything it needs to render and execute the embedded DaVinci experience, but still doesn’t have access to any long-lived credentials, OAuth tokens, or BFF transaction secrets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; tokenRes&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/dvtoken&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	headers: { &lt;/span&gt;&lt;span&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;companyId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;policyId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;apiRoot&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; tokenRes.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; props&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	config: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	method: &lt;/span&gt;&lt;span&gt;&apos;runFlow&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	apiRoot: apiRoot,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	accessToken: token,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	companyId: companyId,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	policyId: policyId&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	useModal: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	successCallback&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	const&lt;/span&gt;&lt;span&gt; result&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; finalizeLogin&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	showAuthenticatedState&lt;/span&gt;&lt;span&gt;(result.user);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;davinci.&lt;/span&gt;&lt;span&gt;skRenderScreen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UI&lt;/span&gt;&lt;span&gt;.widget, props);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important nuance is in the DaVinci flow design. Here’s an example of a flow, with details below:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;DaVinci flow overview showing the full authentication orchestration&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The flow should validate that the BFF-supplied transaction context is present and fresh before continuing with authentication. At minimum, the flow should check that &lt;code&gt;transactionID&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, and &lt;code&gt;expiresAt&lt;/code&gt; are present, that &lt;code&gt;expiresAt&lt;/code&gt; is numeric, and that the current time is not later than &lt;code&gt;expiresAt&lt;/code&gt;. Our flow input schema validation takes care of the first three and we use a simple A&amp;lt;B function node for the fourth.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Flow input validation step 1 - checking transactionID, nonce, and expiresAt fields are present&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Flow input validation step 2 - schema validation configuration&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Flow input validation step 3 - additional schema validation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Flow input validation step 4 - A less than B expiry check&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This validation doesn’t prove that the transaction is valid by itself. The BFF remains the source of truth because it created the transaction. But it gives the DaVinci flow a clean way to reject stale or malformed starts before asking the user to authenticate.&lt;/p&gt;
&lt;h2&gt;Step 3: Generating tokens without returning them to the browser&lt;/h2&gt;
&lt;p&gt;After the user completes the embedded journey, DaVinci needs to generate the OAuth tokens. The critical design rule is that those tokens must not be returned to the widget success callback. Instead, DaVinci should deliver them directly to the BFF using the HTTP Connector’s “Make REST API Call” capability. The key flow design choice here is to ensure that the “PingOne Authentication Return Success Response (Widget Flows)” node is not the final one in the flow.&lt;/p&gt;
&lt;p&gt;To bind the generated token response to the BFF transaction, the ID token should include the BFF-generated nonce and transaction identifier. In the sample implementation, the custom transaction claim is &lt;code&gt;bff_transaction_id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These claims need to be in the ID token. The BFF verifies the signed ID token and uses the claims in that token as proof that the token response is bound to the transaction it originally created.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;ID token custom claims configuration showing bff_transaction_id and nonce fields&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Step 4: Delivering tokens to the BFF server-to-server&lt;/h2&gt;
&lt;p&gt;After DaVinci generates the token response, it calls the BFF directly. This is the key architectural boundary in the design. The browser is never responsible for carrying an authentication artifact from DaVinci to the backend.&lt;/p&gt;
&lt;p&gt;The DaVinci HTTP connector calls &lt;code&gt;/auth/davinci/complete&lt;/code&gt; with a shared callback secret and a JSON body containing the transaction binding values and token response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;POST /auth/davinci/complete&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Content-Type: application/json&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;X-DaVinci-Callback-Secret: &amp;lt;shared callback secret&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;transactionID&quot;: &quot;bff-generated-transaction-id&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;nonce&quot;: &quot;bff-generated-nonce&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;completedAt&quot;: 1717000012345,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;interactionId&quot;: &quot;optional-davinci-interaction-id&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;tokens&quot;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;access_token&quot;: &quot;eyJ...&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;refresh_token&quot;: &quot;optional-refresh-token&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;id_token&quot;: &quot;eyJ...&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;token_type&quot;: &quot;Bearer&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;expires_in&quot;: 3600&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;HTTP connector callback configuration step 1 - Make REST API Call setup&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;HTTP connector callback configuration step 2 - request body mapping&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;HTTP connector callback configuration step 3 - full connector setup&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The BFF route is intentionally registered before the browser-origin middleware, because this request is not coming from the browser and will not have the same Origin and Fetch Metadata headers. Instead, the route is protected using the callback secret, token signature validation, and transaction binding checks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&apos;/auth/davinci/complete&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	callbackRateLimit,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	express.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ limit: &lt;/span&gt;&lt;span&gt;&apos;20kb&apos;&lt;/span&gt;&lt;span&gt; }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	requireDavinciCallbackSecret,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	async&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	const&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;transactionID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nonce&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; req.body &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	const&lt;/span&gt;&lt;span&gt; transaction&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; authTransactions.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(transactionID);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;transaction &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; transaction.status &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &apos;pending&apos;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        	return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;409&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;Unknown or invalid transaction&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;safeEqual&lt;/span&gt;&lt;span&gt;(nonce, transaction.nonce)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        	return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;Invalid transaction binding&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	const&lt;/span&gt;&lt;span&gt; claims&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; verifyIdToken&lt;/span&gt;&lt;span&gt;(tokens.id_token);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	const&lt;/span&gt;&lt;span&gt; tokenTransactionID&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; getTransactionIDFromClaims&lt;/span&gt;&lt;span&gt;(claims);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;safeEqual&lt;/span&gt;&lt;span&gt;(tokenTransactionID, transactionID)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        	return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;Invalid token transaction binding&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;safeEqual&lt;/span&gt;&lt;span&gt;(claims.nonce, transaction.nonce)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        	return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: &lt;/span&gt;&lt;span&gt;&apos;Invalid token nonce binding&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	transaction.status &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;tokens_delivered&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	transaction.idTokenClaims &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; claims;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	transaction.tokens &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tokens;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ received: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code above is intentionally abbreviated, but the validation order is important. The BFF first finds the pending transaction by &lt;code&gt;transactionID&lt;/code&gt;, then checks the &lt;code&gt;nonce&lt;/code&gt; supplied in the body, then verifies the signed ID token and checks that the signed claims contain the same &lt;code&gt;transactionID&lt;/code&gt; and &lt;code&gt;nonce&lt;/code&gt;. This gives us both a server-to-server delivery channel and a signed token-level binding to the transaction.&lt;/p&gt;
&lt;h2&gt;Step 5: Finalizing the BFF application session&lt;/h2&gt;
&lt;p&gt;The server-to-server callback cannot set the browser’s application session cookie because the browser is not involved in that request. That final step still needs to happen from the browser, but the browser doesn’t need to send any sensitive token material.&lt;/p&gt;
&lt;p&gt;When the DaVinci widget completes successfully, the frontend calls &lt;code&gt;/auth/finalize&lt;/code&gt; with an empty JSON body:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; finalizeLogin&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; finalizeRes&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/auth/finalize&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	headers: { &lt;/span&gt;&lt;span&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	body: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;finalizeRes.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    	throw&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Finalize failed with HTTP ${&lt;/span&gt;&lt;span&gt;finalizeRes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; finalizeRes.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This route uses the pre-auth Express session cookie to find the pending transaction that was created when &lt;code&gt;/dvtoken&lt;/code&gt; was called. It then checks that DaVinci has already delivered tokens for that transaction. If the token delivery is complete and all bindings match, the BFF regenerates the Express session and promotes it into an authenticated application session.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; regenerateSession&lt;/span&gt;&lt;span&gt;(req);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.authenticated &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.user &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; user;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.subject &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; transaction.subject;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.access_token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tokens.access_token;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.refresh_token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tokens.refresh_token;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.id_token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tokens.id_token;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;req.session.id_token_claims &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; idTokenClaims;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; saveSession&lt;/span&gt;&lt;span&gt;(req);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This session regeneration step is important. The pre-auth session exists only to bind the browser to the pending authentication transaction. After authentication is complete, the BFF discards that pre-auth session identifier and creates a fresh authenticated session. This reduces session fixation risk and gives the application a clean transition from unauthenticated to authenticated state.&lt;/p&gt;
&lt;p&gt;The browser receives only a normal application response and a secure application session cookie. It never sees the OAuth tokens, the DaVinci API key, or a DaVinci sessionToken.&lt;/p&gt;
&lt;h2&gt;Step 6: Hardening the browser-facing routes&lt;/h2&gt;
&lt;p&gt;The BFF also includes browser-origin checks for the routes that are intended to be called by the frontend. CORS is useful here, but it is important not to overstate what CORS does. CORS is enforced by browsers; it doesn’t authenticate arbitrary non-browser HTTP clients.&lt;/p&gt;
&lt;p&gt;For that reason, the BFF uses origin checks as a browser-side protection layer, not as the only security control. The server-to-server DaVinci callback uses different protection because it is not a browser request.&lt;/p&gt;
&lt;p&gt;For the browser session cookie, the implementation uses an application-scoped cookie with the &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite=Strict&lt;/code&gt; attributes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; sessionCookieOptions&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	httpOnly: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	secure: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	sameSite: &lt;/span&gt;&lt;span&gt;&apos;strict&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	path: &lt;/span&gt;&lt;span&gt;&apos;/&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	maxAge: &lt;/span&gt;&lt;span&gt;SESSION_TTL_MS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cookie is named &lt;code&gt;__Host-acme_session&lt;/code&gt;. The &lt;code&gt;__Host-&lt;/code&gt; prefix gives the browser additional constraints: the cookie must be &lt;code&gt;Secure&lt;/code&gt;, must use &lt;code&gt;Path=/&lt;/code&gt;, and must not include a &lt;code&gt;Domain&lt;/code&gt; attribute. This helps keep the session cookie tightly scoped to the application host.&lt;/p&gt;
&lt;p&gt;This doesn’t eliminate bearer cookies. A BFF application still needs a browser cookie to represent the application session. The point is that the cookie is now an app-local session handle controlled by the BFF, not an OAuth token, refresh token, DaVinci session token, or DaVinci token reference.&lt;/p&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;The full end-to-end flow now looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The browser calls &lt;code&gt;/dvtoken&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The BFF creates &lt;code&gt;transactionID&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, &lt;code&gt;startBy&lt;/code&gt;, and &lt;code&gt;completeBy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The BFF requests a DaVinci SDK token and passes the transaction context into the flow.&lt;/li&gt;
&lt;li&gt;The browser renders the embedded widget using the returned SDK token.&lt;/li&gt;
&lt;li&gt;DaVinci validates the transaction context and authenticates the user.&lt;/li&gt;
&lt;li&gt;DaVinci generates OAuth tokens and includes &lt;code&gt;nonce&lt;/code&gt; and &lt;code&gt;bff_transaction_id&lt;/code&gt; in the ID token.&lt;/li&gt;
&lt;li&gt;DaVinci calls &lt;code&gt;/auth/davinci/complete&lt;/code&gt; server-to-server and delivers the tokens to the BFF.&lt;/li&gt;
&lt;li&gt;The BFF validates the callback secret, pending transaction, body nonce, ID token signature, ID token audience, ID token issuer, ID token nonce, and ID token transaction claim.&lt;/li&gt;
&lt;li&gt;The widget success callback fires in the browser.&lt;/li&gt;
&lt;li&gt;The browser calls &lt;code&gt;/auth/finalize&lt;/code&gt; with no token material.&lt;/li&gt;
&lt;li&gt;The BFF validates the browser session binding and promotes the transaction into an authenticated application session.&lt;/li&gt;
&lt;li&gt;The browser receives only the application session cookie.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;End-to-end flow diagram showing all 12 steps of the BFF authentication pattern&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;While there are a few moving parts, the responsibility boundaries are straightforward. The browser is responsible for rendering the embedded DaVinci widget and collecting the user’s interaction. DaVinci is responsible for executing the identity journey and generating tokens. The BFF is responsible for protecting credentials, validating token delivery, binding the transaction, and establishing the application session.&lt;/p&gt;
&lt;h2&gt;A few practical caveats&lt;/h2&gt;
&lt;p&gt;The sample implementation deliberately keeps the architecture simple, but there are a few production considerations worth calling out.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The sample stores pending authentication transactions in an in-memory Map. That is fine for a single demo service, but production deployments should use Redis, a database, or another shared server-side store.&lt;/li&gt;
&lt;li&gt;The sample uses the default express-session store. That is not suitable for production scale. A production BFF should use a real session store.&lt;/li&gt;
&lt;li&gt;The DaVinci callback currently uses a shared static secret. A stronger version would use an HMAC signature over the request body and timestamp, mTLS, or another stronger server-to-server authentication mechanism where available.&lt;/li&gt;
&lt;li&gt;Origin and CORS checks are useful for browser-based protection, but they do not authenticate non-browser clients. The critical protection for token delivery is the combination of callback authentication, signed ID token validation, and BFF transaction binding.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Over to you&lt;/h2&gt;
&lt;p&gt;It would be great to hear how others are approaching this pattern in their own environments.&lt;/p&gt;
&lt;p&gt;Are you already using a BFF-style architecture to protect OAuth tokens in SPA applications, or are your teams still comfortable managing tokens directly in the browser? When embedding authentication journeys with tools like the DaVinci widget, where do you draw the line between user experience convenience and token-handling risk? And have you found other patterns that preserve the seamless embedded login experience without exposing long-lived credentials or OAuth tokens to frontend code?&lt;/p&gt;
&lt;p&gt;Drop a comment and join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt; with your thoughts, questions, or your own lessons learned from building secure modern web applications in the identity trenches.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.DQK7w9P9_64kfj.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Understanding the Org Model</title><link>https://developer.pingidentity.com/blog/understanding-the-org-model-aic/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/understanding-the-org-model-aic/</guid><description>Learn how PingOne Advanced Identity Cloud and PingIDM&apos;s Organizations enable out-of-the-box extensible, inherited relationships between users and associated groups of users</description><pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/understanding-the-org-model-aic/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;PingOne Advanced Identity Cloud (AIC) and PingIDM (Ping’s self-managed Identity Management) provide a powerful model called Organizations which enables out-of-the-box extensible, inherited relationships between users and associated groups of users. In short, Organizations give you the ability to match your IAM strategy to your business strategy, and not the other way around - they are a means to unify, simplify, and standardize your process across your internal, external, and partner identities.&lt;/p&gt;
&lt;p&gt;Considering its flexibility and scalability, starting with an empty tenant and the base Organization model can feel a bit daunting.&lt;/p&gt;
&lt;p&gt;This Guide intends to teach you the key information you need to know when interacting with AIC/PingIDM’s Organization model. It is split into the following sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#object-modeling-definitions&quot;&gt;Object Modeling Definitions&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#whats-the-deal-with-rdvps&quot;&gt;What’s the Deal with RDVPs?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-the-organization-model&quot;&gt;What is the Organization Model?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-quick-analogy&quot;&gt;A Quick Analogy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#utilizing-the-organization-model&quot;&gt;Utilizing the Organization Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#referencing-the-organization-model-in-the-platform&quot;&gt;Referencing the Organization Model in the Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extending-the-organization-model&quot;&gt;Extending the Organization Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end of this Guide you’ll have a medium-level understanding of Organizations and how they operate generically within AIC and PingIDM.&lt;/p&gt;
&lt;p&gt;This Guide expects a beginner level of familiarity with AIC or PingIDM. Certain topics covered here will be specific to the AIC platform: when this happens, we’ll call it out. If you do have a tenant or deployment available, feel free to follow along however it is not necessary.&lt;/p&gt;
&lt;h1&gt;Object Modeling Definitions&lt;/h1&gt;
&lt;p&gt;Before we define Organizations, we need to understand the underlying infrastructure that surrounds it. Let’s start broadly and then narrow down as we go.&lt;/p&gt;
&lt;p&gt;The Organization model is a part of &lt;strong&gt;Identity Management (IDM)&lt;/strong&gt;. PingIDM is the self-deployed instance and is a core feature set within AIC. Identity Management’s job is to represent, control, connect, and map identity data in and out of the Ping platform. Regarding Organizations, we care most about &lt;em&gt;representing&lt;/em&gt; and &lt;em&gt;controlling&lt;/em&gt; its data - what does the template look like, and ensuring that the data within it is managed appropriately (think CRUDPAQ - Create, Read, Update, Delete, Patch, Action, Query).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-objects/appendix-managed-objects.html&quot;&gt;&lt;strong&gt;Managed Objects&lt;/strong&gt;&lt;/a&gt; are a templatized instance of data in IDM that contains &lt;strong&gt;Properties&lt;/strong&gt;. In essence, they are the representation of identities within IDM. By default, your instance comes with Users, Roles, Assignments, Groups, and Organizations, but you can create as many as you’d like. Managed Objects store data within a Directory, with each Managed Object being categorized separately.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/identities/user-identity-properties-attributes-reference.html&quot;&gt;&lt;strong&gt;Properties&lt;/strong&gt;&lt;/a&gt; (sometimes called Attributes) are the individual data fields within a Managed Object that represent the data types stored within that object. They define the specific pieces of information each instance of that Managed Object will hold. As an example, a User is a Managed Object, and their Username is one of their Properties. Properties can be booleans, numbers, arrays (called &lt;em&gt;multivalues&lt;/em&gt;), objects (including nested objects), strings, &lt;strong&gt;Relationships&lt;/strong&gt;, and &lt;strong&gt;Virtual Properties&lt;/strong&gt;. You can also &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-objects/creating-modifying-managed-objects.html#&quot;&gt;create custom types&lt;/a&gt; as needed. Properties have their own unique indexing&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, validation, hashing or encryption, and triggers which can be performed on actions such as validating, retrieving, or storing an instance of this Property.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-objects/relationships-custom.html&quot;&gt;&lt;strong&gt;Relationships&lt;/strong&gt;&lt;/a&gt; are a type of Property that creates a linkage between instances of Managed Objects. Relationships can be one-to-one, one-to-many, many-to-one, and many-to-many, and can be linked to different Managed Objects or the same Managed Object. Common examples of relationships include Manager to Employees, Parent to Children, or Department to Teams. Relationships can contain their own data within the link itself, can return their data alongside either end of the link in a query, and can be used as a means to &lt;strong&gt;notify&lt;/strong&gt; the connected Managed Objects that changes have occurred on opposite ends of the link (&lt;strong&gt;Relationship-Derived Virtual Property&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-objects/managed-object-virtual-properties.html&quot;&gt;&lt;strong&gt;Virtual Properties&lt;/strong&gt;&lt;/a&gt; are a powerful Property type that can dynamically calculate its own data on the fly, either on time of retrieval or based on notifications received from a Relationship tied to this Managed Object (&lt;strong&gt;Relationship-Derived Virtual Property&lt;/strong&gt;). This significantly simplifies data and queries as well as improves performance because the complex calculations are handled for you dynamically at run time or statically at storage time - for example, you could have a virtual property that dynamically returns the “Full Name” of a User as a combination of their First and Last name without actually having to store and update that value separately.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-objects/managed-object-virtual-properties.html#relationship-derived-virtual-properties&quot;&gt;&lt;strong&gt;Relationship-Derived Virtual Properties (RDVPs)&lt;/strong&gt;&lt;/a&gt; are a type of Virtual Property that update based on notifications from the Relationships linked to its Managed Object. An RDVP has access to content from the Object’s Relationships and can travel the chain of relationships to create a composite value of the chain. Relationships and RDVPs are the two components of IDM that make Organizations so flexible and capable.&lt;/p&gt;
&lt;h2&gt;What’s the Deal with RDVPs?&lt;/h2&gt;
&lt;p&gt;RDVPs are an insanely powerful tool for reducing complexity and increasing performance.&lt;/p&gt;
&lt;p&gt;As an example, let’s say that you have a business structure of Managers to Employees like we mentioned in the Relationships section. In most companies, you’re not going to have one layer of Managers - you’ll probably have at least 3 and each of those layers may have tens to hundreds of Managers who then manage their own number of Employees.&lt;/p&gt;
&lt;p&gt;If I wanted to get a list of all of the Managers above an Employee, how would I go about it? Normally, it would likely be a series of queries that would look something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the Employee&lt;/li&gt;
&lt;li&gt;Find the Employee’s Manager&lt;/li&gt;
&lt;li&gt;Get the Manager&lt;/li&gt;
&lt;li&gt;Find the Manager’s Manager&lt;/li&gt;
&lt;li&gt;Get the Manager’s Manager&lt;/li&gt;
&lt;li&gt;Find the Manager’s Manager’s Manager&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;li&gt;Manager X has no Manager, return&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think about this the other way, too. If I wanted to get all of the Employees &lt;em&gt;under&lt;/em&gt; a Manager, and each Manager has more than one Employee, how many operations do I have to take? Now what if I had to perform this action every time I entered an application? This is starting to sound a lot like one of those &lt;a href=&quot;https://www.geeksforgeeks.org/top-50-graph-coding-problems-for-interviews/&quot;&gt;classic graph traversal interview questions&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The deeper or wider you go, with the more connections you have, the more operations you would normally have to take. This makes your code complex and significantly increases the number of queries you need to make to get the data you and your users need to be successful. &lt;strong&gt;RDVPs solve this problem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDVPs travel the Relationships for you and update a static result on the object directly at time of change from anywhere in the Relationship.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If we looked at the previous example, if I wanted to get a list of the Employee’s Managers, or a list of the Manager’s Employees, &lt;strong&gt;the data is on the User directly&lt;/strong&gt;. If the Employee is promoted, or switches teams, and the managerial hierarchy changes, &lt;strong&gt;the data on the User updates automatically&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDVPs proactively updates relevant aggregate data so that you only have to look at one Object to understand what’s going on with everything it’s linked to.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;What is the Organization Model?&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/identities/organizations.html&quot;&gt;&lt;strong&gt;Organizations&lt;/strong&gt;&lt;/a&gt; are a &lt;strong&gt;Managed Object&lt;/strong&gt; that contain multiple &lt;strong&gt;Relationships&lt;/strong&gt; both to Users and to other Organizations. These Relationships store &lt;strong&gt;RDVPs&lt;/strong&gt; that define the linkages that the Organization has with other Organizations and Users. Since Organizations are a Managed Object, they are adherent to the same scale as Users, meaning that AIC is rated for hundreds of millions of Organizations while PingIDM is hypothetically as scalable as your hardware will allow.&lt;/p&gt;
&lt;p&gt;Organizations, at their core, build a &lt;strong&gt;hierarchy&lt;/strong&gt; of templatized data. Due to the Relationships between Organizations and Users, this data can be &lt;strong&gt;inherited&lt;/strong&gt; up and down the hierarchy.&lt;/p&gt;
&lt;p&gt;Visualized, the Organization model often looks like a tree.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Organization Model&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Organizations enable hierarchical, inherited data templates&lt;/strong&gt;, allowing you to &lt;strong&gt;standardize and represent complex structures&lt;/strong&gt; such as families, business partnerships, and supplier/distributor models.&lt;/p&gt;
&lt;p&gt;But just having this data is only one part of what makes this model special. Organizations don’t just represent Parent/Child relationships between other Organizations: they also by default contain 3 different Relationship types with Users - and Users can retrieve and inherit information from the hierarchy as well. Due to the RDVPs already on the Organization, and the permission model in place on the Organization, elevated Relationship types (i.e. Owners and Admins) have the ability to manage aspects of both the Organization, its related Users, and any Organizations and Users that they’ve inherited - both via REST API and (in the case of AIC) hosted UI.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Organization Model with Users&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Organizations enable hierarchical, inherited delegated administration&lt;/strong&gt;, allowing you to &lt;strong&gt;scale administration alongside your platform&lt;/strong&gt; by delegating operational management of the hierarchy and its Users to the Users within the Organization (or its Parent).&lt;/p&gt;
&lt;p&gt;Additionally, these Relationship types are &lt;strong&gt;many to many&lt;/strong&gt;, meaning that a single User can be a Member, Admin, and Owner of as many different Organizations and Hierarchies as they need.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Universal Identity in Organizations&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Organizations enable a Universal Identity&lt;/strong&gt;, where &lt;strong&gt;one Identity has one User account for all Services&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So, why Organizations?&lt;/p&gt;
&lt;p&gt;Organizations let you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Templatize grouping of content so that you can build a standard that the platform adheres to&lt;/li&gt;
&lt;li&gt;Delegate the content so that the platform can be managed at scale without having to hire thousands of admins to keep up&lt;/li&gt;
&lt;li&gt;Link the content so that it and its users can inherit information and administration for quicker time-to-value&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;A Quick Analogy&lt;/h2&gt;
&lt;p&gt;Think of the Organization Model as a Condominium.&lt;/p&gt;
&lt;p&gt;As the Tenant Administrator, you’re the owner of the Building itself. You dictate what services are available, how the condos are laid out, and the keycard system for how the residents get into the building. You can also add floors and rooms to the Condo at any time.&lt;/p&gt;
&lt;p&gt;The people in your Condo (Users) could be Residents (Admins) or Staff (Owners). Everyone must use their keycard to get into the building, and that same keycard may give them access to different rooms or Condos in the building. A Resident, for example, may have access to their Condo as well as to a friend’s Condo. Staff may have access to all Condos on a given floor, and may have access to systems like plumbing and electrical which the Residents don’t interact with.&lt;/p&gt;
&lt;p&gt;Residents can modify their Condo, but how they can change it must adhere to the rules and regulations defined by the Building. To the benefit of the Resident, ongoing maintenance of the Condo and its services are managed by the Building and not by them.&lt;/p&gt;
&lt;p&gt;If the Resident decides to rent out their Condo, their Tenants (Members) will have access to the Building and to the Condo but won’t be able to modify the Condo at all. They can only use the facilities given to them by the Resident.&lt;/p&gt;
&lt;p&gt;As your Building grows, you may need more staff to run it. As such, you can appoint Floor Managers (Owners of a Parent Org) who also have the ability to create new Condos and manage the Floors,  Residents, and Tenants within.&lt;/p&gt;
&lt;p&gt;Additionally, some of your Maintenance Staff and Floor Managers live in your Building. That means they are also Residents, and have different capabilities and responsibilities depending on what Condo they’re currently in.&lt;/p&gt;
&lt;h1&gt;Utilizing the Organization Model&lt;/h1&gt;
&lt;p&gt;So let’s put this all together and see how the Organization Model works in practice. Note that the platform screens shown below are taken from the built-in Hosted Pages within AIC, however each and every action is available in PingIDM via the Native UI or REST API.&lt;/p&gt;
&lt;p&gt;Our goal today will be to create a basic business hierarchy. We’ll take the Supplier/Distributor model for this example.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Supplier/Distributor Example&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In our example we have a Manufacturing vertical named &lt;strong&gt;BX Manufacturing&lt;/strong&gt;. BX Manufacturing has partnerships with &lt;strong&gt;Suppliers&lt;/strong&gt;, who provide goods and services, and &lt;strong&gt;Distributors&lt;/strong&gt;, who purchase BX Manufacturing’s products to then be sold elsewhere. Both Suppliers and Distributors need to give their employees access to BX Manufacturing’s systems but what they ultimately do with those systems is very different.&lt;/p&gt;
&lt;p&gt;To start, let’s create the Root Organization. As the Tenant Admin, we can select “New Organization”, enter in the Name, and then we’re ready to go.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Creating the Root Organization&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, an Organization is only as useful as its Relationships. With that in mind, let’s assign an Owner to this Organization so that we can delegate management of the Manufacturing Vertical and its subdivisions.&lt;/p&gt;
&lt;p&gt;Underneath the “Owner” section, we can assign a User to our Organization. In my example, the user “Owner” has already been registered. Note that while we are assigning manually here, you can always automate this assignment during provisioning, user registration (likely via a Journey/Tree in AIC/PingAM), or (if you have Governance features in your AIC tenant) during Access Request or with a Form submission that triggers a Workflow or Approval.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Assigning the Owner&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let’s log in as the Owner User. You’ll find that you have access to update and delete BX Manufacturing and its users and can create new Organizations underneath the Organization you’re an Owner of.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Default Owner Permissions&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;While you have very similar capabilities to the Tenant Administrator by default, you won’t have access to manage other Owners - that’s the job of the Tenant Administrator.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Default Owner Permissions - No Owner Management&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since we’ve been delegated ownership of BX Manufacturing, let’s set up the Suppliers and Distributors suborganizations and assign some people to manage them. We’ll create a new Organization and assign that Organization to BX Manufacturing as a Parent.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Creating the Sub-Organizations (Children)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We’ll repeat the same action for the Distributors Organization.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating the First-Level Children&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Currently, our Organizations don’t have any Users in them. Let’s fix that. As an Owner, we can create a new User and assign them as a Member and/or an Administrator to any and all Organizations that we own and have inherited ownership to.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Creating the Suppliers Administrator&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If we log out as an Owner, and log in as the Suppliers Admin, we’ll see that we only have access to the Organization that we were assigned, and unlike the Owner can’t manage other Administrators of our Organization(s).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Default Administrator Permissions&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We &lt;em&gt;can&lt;/em&gt;, however, manage our Users and create and manage new Organizations. Let’s create Supplier A and Supplier B now as children of Suppliers.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Creating the Suppliers&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Additionally, let’s create some Users who are Members of the different Suppliers.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Assigning Members&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Log back in as the Owner. You’ll see that you have inherited permissions to the Suppliers and the Users made by your delegated Administrator.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Inheritance at Work&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Owners and Admins&lt;/strong&gt; &lt;strong&gt;inherit permission&lt;/strong&gt; to all of their sub-Organizations and its Users. &lt;strong&gt;Members inherit context&lt;/strong&gt; from all of their Parent Organizations. &lt;strong&gt;Owners and Admins permission goes down the tree, Members context goes up.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We mentioned before that Users can have more than one Relationship to one or more Organization. Let’s say, for example, that Supplier B interacts with Distributor A to sell OEM add-ons to a customer, like a spoiler on a car. Since BX Manufacturing is the creator and owner of the finished product, they must be intrinsically involved in the linkage between this Supplier and Distributor. Here we’re dealing with a &lt;strong&gt;B2B2C&lt;/strong&gt; scenario, or Business-to-Business-to-Consumer.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Unified Identity&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this situation, our User is an Admin to Supplier B and is also a Member to Distributor A. At the end of the day, this is the same person with the same account and the same login but with different relationships to each Organization. One person is tied to one identity, but the Relationships that they have to the Organizations give them different permissions to Organizations and Users and give others different permissions to them.&lt;/p&gt;
&lt;p&gt;In practice, as the user Supplier B Admin I can see Supplier B and the Members associated with Supplier B. &lt;strong&gt;As Distributor A Admin, I can see and manage Supplier B Admin because they’re a Member of my Organization.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My Supplier received the context they needed to interact with Distributor while still being able to manage their own Users and Supplier information. My Distributor cannot receive the context of the Supplier but can manage the suppliers associated with their Organization.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;
&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;B2B2C Using Organizations&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Referencing the Organization Model in the Platform&lt;/h1&gt;
&lt;p&gt;Advanced Identity Cloud takes the Organization Model one step further and incorporates it throughout its platform. That way, actions and interactions can reference the context and relationships to make unique experiences and define specific permissions defined by each individual user.&lt;/p&gt;
&lt;p&gt;To start, the &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/idm-scripting/scripting-func-engine.html&quot;&gt;openidm binding&lt;/a&gt; allows you to access and manage any IDM data throughout AIC. This gives you the flexibility to interact with your Organization throughout the User journey, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An Event Hook that auto-assigns Organization membership based on a Property or Group they’re a member of (for example, a specific AD OU)&lt;/li&gt;
&lt;li&gt;A Custom Endpoint that returns Relationship information for decentralized Policy Enforcement&lt;/li&gt;
&lt;li&gt;User Journeys where things like branding, MFA, and risk policies are dictated by the configuration on the Organization&lt;/li&gt;
&lt;li&gt;Access Token Modification where the token includes context of what Organizational privileges the User has&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To give you a sense of what actions are useful in the context of Organizations, take a look at this &lt;a href=&quot;https://github.com/gwizdala/lib-ping/blob/main/Library%20Scripts/library_org.js&quot;&gt;Library Script&lt;/a&gt; that you can use wherever Next-Gen scripting is available.&lt;/p&gt;
&lt;p&gt;If you have Governance enabled within your AIC Tenant, Organizations appear in the following places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owners and Admins can request access to Applications, Entitlements, and Roles on behalf of their Members.&lt;/li&gt;
&lt;li&gt;Identity Certification Campaigns can be filtered to Users in specific Organizations&lt;/li&gt;
&lt;li&gt;Scopes can be filtered to specific Organizations and Relationships (e.g. Owner/Admin/Member)&lt;/li&gt;
&lt;li&gt;Forms allow a dynamically enumerated list of Organizations as a Multi-select option&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Just like with the rest of the platform, you can utilize the same bindings to interact with the Organization in actions such as Workflows.&lt;/p&gt;
&lt;h1&gt;Extending the Organization Model&lt;/h1&gt;
&lt;p&gt;So where do we go from here?&lt;/p&gt;
&lt;p&gt;Organizations give us a template with pre-built relationships that can be referenced throughout Ping’s platform. And that’s just it: it’s a &lt;strong&gt;template&lt;/strong&gt; upon which we can extend and expand to meet the needs of our organization.&lt;/p&gt;
&lt;p&gt;As a start, add Properties to your Organization - things you see your business units and partnerships doing over and over again that could (and should) be standardized.&lt;/p&gt;
&lt;p&gt;Once you’ve built that model the way you like it, connect the model to your platform. It could be &lt;a href=&quot;https://gwizkid.com/posts/dynamically-branding-journeys-in-aic/&quot;&gt;a way to control branding&lt;/a&gt;, &lt;a href=&quot;https://gwizkid.com/posts/managing-mfa-in-journeys/&quot;&gt;extended support capabilities&lt;/a&gt;, or something completely different - work with your stakeholders to decide what is important for them and their users to &lt;strong&gt;have&lt;/strong&gt; and to &lt;strong&gt;manage&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When you’ve got the model in a place you like, &lt;a href=&quot;https://gwizkid.com/posts/setting-delegated-admin-privileges-in-aic/&quot;&gt;delegate administration&lt;/a&gt; to the capabilities you’d like to share with your partnerships. You own the model, and you delegate the controls.&lt;/p&gt;
&lt;p&gt;If you see that the parent organizations need more control than the children, consider &lt;a href=&quot;https://gwizkid.com/posts/modeling-inheritance-in-aic-or-idm/&quot;&gt;applying inheritance rules&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And, as you expand, keep changing it! The beauty of this template is that it grows with you: you can enable the right experience, security, lifecycle, and overall IAM strategy for each and every use case without having to rebuild your entire platform every time a new business requirement comes along.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Through this Guide we have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learned about the key elements of IDM that make up the Organization model&lt;/li&gt;
&lt;li&gt;Learned how the Organization model applies these concept&lt;/li&gt;
&lt;li&gt;Created, delegated, and managed Organizations and Users&lt;/li&gt;
&lt;li&gt;Learned how to connect Organizations throughout AIC&lt;/li&gt;
&lt;li&gt;Learned about common approaches in extending the Organization model to meet our business needs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Organization model provides you the bricks to build dynamic, magical experiences for your Administrators and your Users. And with this foundation the only limit you have is your imagination.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;section&gt;&lt;h2&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Note that AIC provides &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/identities/identity-cloud-identity-schema.html#use-general-purpose-extension-attributes&quot;&gt;general purpose extension attributes&lt;/a&gt; for cases in which you need to index additional values as a means to protect tenant response time. This is not a limitation in PingIDM. &lt;a href=&quot;#user-content-fnref-1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.BCZOVrjw_3pGez.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>PingOne DaVinci Primer: Creating Dynamic Experiences with PingOne Populations</title><link>https://developer.pingidentity.com/blog/davinci-primer-dynamic-populations/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/davinci-primer-dynamic-populations/</guid><description>Learn how to build dynamic experiences using PingOne context in PingOne DaVinci</description><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/davinci-primer-dynamic-populations/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;As part of creating a User in PingOne that User will need to be assigned to a &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_populations.html&quot;&gt;Population&lt;/a&gt;. Populations in PingOne create logical segments of user identities, which not only define administrative boundaries for Users and Groups with elevated permissions (Admin Roles) but also provide additional metadata that can be used to cater the User’s experience to fit their unique use cases.&lt;/p&gt;
&lt;p&gt;This How-To will teach you how to create dynamic user experiences using PingOne’s built-in Populations coupled with PingOne DaVinci. Along the way you’ll learn common strategies and design decisions when building out PingOne DaVinci flows.&lt;/p&gt;
&lt;p&gt;The level of detail here is meant for beginners to PingOne and PingOne DaVinci. If you are just looking for, or want to follow along with, the pre-built example, download the DaVinci flow using the link &lt;a href=&quot;https://github.com/gwizdala/lib-ping/blob/main/How-Tos/davinci-primer-dynamic-populations/dynamic_experiences_with_populations.json&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are DaVinci tips and tricks interspersed throughout this document. Search for the ⭐ emoji to jump between them.&lt;/p&gt;
&lt;p&gt;This How-To is broken down into the following sections&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#initial-setup&quot;&gt;Initial Setup&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#creating-the-forms&quot;&gt;Creating the Forms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#creating-your-davinci-flow&quot;&gt;Creating Your DaVinci Flow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#retrieving-the-population&quot;&gt;Retrieving the Population&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#retrieving-the-population-based-on-the-user&quot;&gt;Retrieving the Population based on the User&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#collecting-the-username&quot;&gt;Collecting the Username&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finding-the-user&quot;&gt;Finding the User&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#retrieving-the-population-details&quot;&gt;Retrieving the Population Details&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#retrieving-the-population-based-on-the-alternative-identifier&quot;&gt;Retrieving the Population based on the Alternative Identifier&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#parsing-the-email-domain&quot;&gt;Parsing the Email Domain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#selecting-a-population-from-a-list&quot;&gt;Selecting a Population from a List&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#changing-the-theme-based-on-the-population&quot;&gt;Changing the Theme based on the Population&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#enforcing-password-policies-based-on-the-population&quot;&gt;Enforcing Password Policies based on the Population&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#enforcing-login-with-an-external-idp-based-on-the-population&quot;&gt;Enforcing Login with an External IdP based on the Population&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dynamic-registration&quot;&gt;Dynamic Registration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#connecting-the-flow-to-your-applications&quot;&gt;Connecting the Flow to Your Application(s)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#other-helpful-tricks&quot;&gt;Other Helpful Tricks&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#variables&quot;&gt;Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#parsing-objects-from-a-teleport&quot;&gt;Parsing Objects from a Teleport&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end of this How-To you’ll have a DaVinci flow that gathers Population metadata based on the User and Population context and applies that metadata during the User’s login and registration experience. Along the way, you’ll learn a wide variety of DaVinci capabilities such as (but not limited to):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigating the DaVinci canvas&lt;/li&gt;
&lt;li&gt;Building and using Forms within the page&lt;/li&gt;
&lt;li&gt;Interacting with Connectors, Triggers, and Action Decision Nodes&lt;/li&gt;
&lt;li&gt;Passing data between nodes&lt;/li&gt;
&lt;li&gt;Branching and teleporting during the user’s journey&lt;/li&gt;
&lt;li&gt;Finding and using nodes and flows from the Marketplace&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This How-To expects a beginner level of familiarity with DaVinci and the PingOne console. Since &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_manage_populations.html&quot;&gt;Managing Populations&lt;/a&gt; is well-defined in the documentation, we won’t be discussing that here.&lt;/p&gt;
&lt;h1&gt;Initial Setup&lt;/h1&gt;
&lt;p&gt;This How-To requires an Environment with PingOne SSO and PingOne DaVinci. Make sure you have the appropriate Environment, licenses, and permissions before continuing.&lt;/p&gt;
&lt;p&gt;You can use the Default Population for this How-To, or you can create a new one: just make sure you have at least one User in that Population so that you can test your flows. The name of my Population is “Example” and my user is “Example User” with the username “&lt;a href=&quot;mailto:example@bxretail.org&quot;&gt;example@bxretail.org&lt;/a&gt;”.&lt;/p&gt;
&lt;p&gt;Fortunately for us, importing and exporting experiences like the one in this document is trivial in PingOne. If you’d like to start with the pre-built example, download the DaVinci flow using the link &lt;a href=&quot;https://github.com/gwizdala/lib-ping/blob/main/How-Tos/davinci-primer-dynamic-populations/dynamic_experiences_with_populations.json&quot;&gt;here&lt;/a&gt; and then import into your Environment to get going.&lt;/p&gt;
&lt;p&gt;That being said, if you’re looking to get a better understanding of &lt;em&gt;how&lt;/em&gt; to construct flows like this, it’s worth the time to follow along with this How-To.&lt;/p&gt;
&lt;h2&gt;Creating the Forms&lt;/h2&gt;
&lt;p&gt;Since we are going to be capturing some user information to log in and are going to be branding them based on our Population, we should create some &lt;a href=&quot;https://docs.pingidentity.com/pingone/user_experience/p1_forms.html&quot;&gt;Forms&lt;/a&gt;. Forms provide a drag-and-drop editor for building user interfaces that we can then reuse throughout our DaVinci Flows.&lt;/p&gt;
&lt;p&gt;Go to “User Experience” - “Forms” in your PingOne navigation bar. You’ll see a series of example forms that can be used in your flows - feel free to click each of them to see what capabilities they provide in the preview.&lt;/p&gt;
&lt;p&gt;Our experience is going to collect the user’s username and then use that information to figure out what Population they are associated with either for registration or authentication. We can quickly create that form by duplicating the &lt;strong&gt;Example - Sign On&lt;/strong&gt; form already made for us.&lt;/p&gt;
&lt;p&gt;Select the form entitled &lt;strong&gt;Example - Sign On&lt;/strong&gt;, click on the three-dot menu (fun fact: those are called &lt;a href=&quot;https://van-ons.nl/en/blog/functioneel-ontwerp/wat-hamburgers-kebab-en-ux-met-elkaar-te-maken-hebben/&quot;&gt;Kebab Menus&lt;/a&gt;) and select &lt;strong&gt;Duplicate&lt;/strong&gt;. You’ll see a new version created called something like &lt;strong&gt;Example - SignOn1&lt;/strong&gt; - select it, hit the three dots and this time click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating Your First Form&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Editor view gives us access to a bunch of different capabilities, but we’re not going to need to make a whole lot of changes for our use case.&lt;/p&gt;
&lt;p&gt;First, under Properties (in the left bar) change the Form Name to &lt;strong&gt;Email-First Authentication&lt;/strong&gt;. Then, select the Password field and click on the trash can icon in the left window to delete it. Optionally, select the Username field, change the validation type to “Custom”, the Regex to &lt;code&gt;^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$&lt;/code&gt; and the Error Message to the translation key &lt;code&gt;forms.fields.user.email.errorMessage&lt;/code&gt; (which is the phrase “Invalid Email Address”).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Email-First Authentication Form&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When you’re done, make sure to save the form by hitting the “Save” button in the top-right of the page.&lt;/p&gt;
&lt;h2&gt;Creating Your DaVinci Flow&lt;/h2&gt;
&lt;p&gt;We’re going to be retrieving and responding to our Users’ inputs within a DaVinci flow.&lt;/p&gt;
&lt;p&gt;On your left navbar, click on the DaVinci button. If it’s not there, make sure you’re in an Environment with DaVinci enabled.&lt;/p&gt;
&lt;p&gt;A new tab will open in your browser to DaVinci. Click on the “Flows” button on the left navigation bar and the “Add Flow” in the top right corner of the page. If you’re using the example flow, feel free to select “Import Flow” otherwise click the “Blank Flow” option from the dropdown, giving it the name &lt;strong&gt;Dynamic Experiences with Populations&lt;/strong&gt;. Once you hit “Create”, you’ll be taken into the DaVinci editor, which is where we’ll spend the majority of this How-To.&lt;/p&gt;
&lt;h1&gt;Retrieving the Population&lt;/h1&gt;
&lt;p&gt;Our Population contains important information about our user - things like their password policy, their branding, and if they log in using an external identity provider. To use that information accordingly we need a way to figure out what population our user (or new user) is related to.&lt;/p&gt;
&lt;h2&gt;Retrieving the Population based on the User&lt;/h2&gt;
&lt;h3&gt;Collecting the Username&lt;/h3&gt;
&lt;p&gt;First off, let’s pull Population data based on the User that we’ve identified in our DaVinci flow. Think of this use case as the initial login page for your customers into your business: based on who we see, we’ll dynamically change their experience accordingly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Connectors, Capabilities, and Triggers&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To start, click the “+” button in the bottom-left corner of your Canvas (that’s the editor window), click “User Interface”, and click on your “Email-First Authentication” form. A little node will snap to your mouse which you can then click on the canvas to add.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Adding a New Form&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The form is a Connector and is using the “Show Form” capability - you can check this out by clicking the back error at the top of the form editor screen. You’ll also see that you can select the forms you created previously from the “Form” dropdown that appears.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Selecting the “Show Form” Capability&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At this point you have a single node that collects the username and stores it under the key &lt;code&gt;user.username&lt;/code&gt;. To test, hit the “Deploy” button in the top right corner and then the “Try Flow” button to open up the form in a new tab.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A rendered Form&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Nice! In one node you have a log in form. Now, let’s figure out what Population this user is from based on their username.&lt;/p&gt;
&lt;h3&gt;Finding the User&lt;/h3&gt;
&lt;p&gt;Click and drag from the black dot on the right side of the Form connector - a little line should show up. When releasing that line, you’ll be prompted to add a new connector. This time, select the PingOne connector.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Connecting Connectors&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You’ll see that your Form is now connected to a new node with a little grey bubble and a blue line with the word “True” on it. Those bubbles are called &lt;strong&gt;Action Decision Nodes&lt;/strong&gt; and they allow us to branch decisioning logic based on the results of the prior connectors. Right now, that blue “True” is saying that if &lt;strong&gt;All Triggers are True&lt;/strong&gt;, then the Form will continue to the PingOne connector. You can have multiple responses come from the same action decision node, which we’ll use later when handling actions like a user not existing in our Environment. If you ever need to change a trigger, you can right-click the word (in that case, “True”) or click on the Action Decision Node directly.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Action Decision Nodes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now that these two connectors are connected, let’s configure the PingOne node to lookup the user. Click on the PingOne connector and select the “Find User” capability.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Passing Data with Handlebars&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We are going to be looking up our user based on their username, using the username inputted by the form. To do so, under the &lt;strong&gt;PingOne Attributes&lt;/strong&gt; field type the word &lt;code&gt;username&lt;/code&gt; and hit enter. Then under the identifier, select the “{}” icon in the right side of the input and click on your Form. Any time you see the “{}” icon means that you have access to data collected along the course of your DaVinci flow.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Collecting Input Data&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once you’ve clicked on your Form connector, you’ll see that the output values include the username keyed based on the defined key we set in the Form Connector. Go ahead and select &lt;code&gt;user.username&lt;/code&gt; as the Identifier.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Selecting the specific value&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you’re curious why the icons for these values are curly braces, it’s because the data is actually being referenced using &lt;a href=&quot;https://handlebarsjs.com/&quot;&gt;handlebars notation&lt;/a&gt; - if you hover on the value you selected you can see what the reference looks like. For the most part, you’ll probably not have to work with the handlebars directly, but it’s useful to know in cases that you want to ensure a specific value is being used in connectors downstream.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Seeing the Handlebars representation of the data&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; Flows can get quite large quite fast. Consider putting unique titles and descriptions on each connector (you can find that in the “Settings” tab within the action’s editor view) as well as annotations (right-click on the canvas and select “Annotation”) to help you reference the correct values in inputs as well as better understand what a flow is doing when maintaining/editing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hit “Apply” to save your changes.&lt;/p&gt;
&lt;h3&gt;Retrieving the Population Details&lt;/h3&gt;
&lt;p&gt;If a User is found, we know their Population.&lt;/p&gt;
&lt;p&gt;From the “Find User” connector, add an “All Triggers True” action connection to an Http connector with the capability “Custom HTML Message”. The message should say “Your population ID is” with the added variable of the matchedUser.population.id.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Displaying the Population ID&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Apply, Deploy, and then Try your new flow. If you use a pre-existing user, you should see their Population ID.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The matched user’s Population ID&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A Population ID isn’t too useful by itself. Let’s pull data on the &lt;strong&gt;Population&lt;/strong&gt; directly using the ID we’ve retrieved.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Disabling Nodes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Move the Display Population ID connector out of the way, right click, and &lt;strong&gt;Disable&lt;/strong&gt; the connector. This lets us keep the connector in our editor if we want to use it later without it being considered by DaVinci as a valid path. The node will turn semi-transparent to indicate that it’s disabled.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Disabling a Node&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From the same Action Decision Node, connect a PingOne Connector with the capability “Read Population”. In your input, add the same population ID from your matched user that you used in the Display Population step.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Reading a Population&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, if the Population can be found, we can pull more metadata from that Population. Connect a new Http connector with the “Custom HTML Message” capability, toggle on “Show Continue Button”, and add the Message “Your Population data:” with the additional value of the &lt;code&gt;population&lt;/code&gt; output that came from the Read Population node. If you want to add an object or array, rather than a single piece of data, into an input click the “+” button next to the specific object value.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding an Object as an Input Value&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Apply, Deploy, and Try your flow. This time, you should see all of the metadata that came back from your user’s population.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The User’s Population&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Retrieving the Population based on the Alternative Identifier&lt;/h2&gt;
&lt;p&gt;Next, let’s get the Population based on an Alternative Identifier we’ve been provided during the flow. Alternative Identifiers are custom values that you can set on a Population and are used to help you identify what Population should be selected during a flow. Think of this as a domain (possibly in a redirect URL or in the user’s email address), an ID (in a query parameter or body of a REST call), or any other value you’d like to use to help you recognize what Population matches. This is a great approach when you want specific users to be registered into particular populations as you can identify them even before they have created an account in PingOne.&lt;/p&gt;
&lt;p&gt;In fact, registering by context is exactly what we want to add into our flow. Currently, if you enter in a user that doesn’t exist in PingOne, the flow returns an error. Instead, let’s try to find the right population for this user: falling back to our Default population if nothing matches.&lt;/p&gt;
&lt;h3&gt;Parsing the Email Domain&lt;/h3&gt;
&lt;p&gt;We are going to attempt to match the email domain extracted from our user’s username to an &lt;strong&gt;Alternate Identifier&lt;/strong&gt; within our Population.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Ping Marketplace&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You probably noticed that when you got the user information, the domain isn’t extracted by default. If you wanted, you could always write a custom function in a Functions Connector to do this for you: but rather than build from scratch, Ping provides a &lt;a href=&quot;https://marketplace.pingone.com/home&quot;&gt;Marketplace&lt;/a&gt; of Nodes and Flows pre-configured to handle the heavy-lifting for you. In this case, let’s use the &lt;a href=&quot;https://marketplace.pingone.com/item/parse-domain-from-email-address&quot;&gt;Parse Domain from Email Address node&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Log into the Marketplace with your PingOne admin credentials, hit the “Copy to clipboard” button in the top right corner of the Marketplace listing, and then in your DaVinci canvas right-click an empty area and select “Paste Nodes”. You’ll now have the connector ready to go in your flow.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Pasting Nodes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a rule of thumb: if there’s functionality you want but you can’t find a trigger that does it, a custom page in HTML that you want designed, or a more complicated flow that you don’t know how to start on, go to the Marketplace. It’s likely there’s something there that will either do or help you get to the solution you’re looking for.&lt;/p&gt;
&lt;p&gt;Some notable Marketplace listings include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nodes:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.pingone.com/item/user-registration-node-block&quot;&gt;User Registration Node Group&lt;/a&gt;: A set of nodes that complete a registration process. Useful when learning how to design HTML in DaVinci, action on button presses with the A Equals Multiple B trigger, and interacting with teleports.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Styling:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.pingone.com/item/ping-ux-css&quot;&gt;Ping UX - DaVinci CSS&lt;/a&gt;: A set of CSS files to help you quickly stylize and brand your custom HTML pages, compatible with Bootstrap formatting.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.pingone.com/item/davinci-design-studio&quot;&gt;DaVinci Design Studio&lt;/a&gt;: A Chromium web plugin that lets you quickly create custom themes in CSS that you can add as a custom style to your DaVinci flows. If you’re using custom HTML rather than a Form, and don’t have your own CSS to use, this is a great starting point.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Flows:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.pingone.com/item/davinci-dna-workforce-identity-solution&quot;&gt;DaVinci DNA Workforce Solution&lt;/a&gt; and &lt;a href=&quot;https://marketplace.pingone.com/item/davinci-dna-ciam-solution&quot;&gt;DaVinci DNA CIAM Solution&lt;/a&gt;: A comprehensive set of modular functionality built out in reusable subflows, including PingOne MFA, PingID, PingOne Protect, and PingOne Verify. This is the gold standard for how to design flows and should get you 90% of the way there when building out your own designs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.pingone.com/item/verified-trust-for-workforce-helpdesk-solution&quot;&gt;Verified Trust for Workforce&lt;/a&gt;: A flow that enables a helpdesk user to confirm someone’s identity before performing sensitive actions. These flows are also built on the DNA methodology.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alright: enough about the Marketplace. Let’s wire up this new node.&lt;/p&gt;
&lt;p&gt;Connect the Action Decision Node off of your Find User node to the Parse Email Domain node, right-click the “True” action connection and switch it to “Any Trigger False”. In other words, we’re telling DaVinci to parse the email domain when the user does not exist in PingOne. Finally, within the Parse Email Domain node set the value of the variable &lt;strong&gt;email&lt;/strong&gt; to the username you collected within the form.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Parsing the Email Domain&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hit “Apply”, and then drag and create a new PingOne Connector off of your Parse Domain node. From the new node select the “Read Population” trigger again but this time change the Population search to “Use Alternative Identifier” and pass in the domain that was parsed in the prior node.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Reading the Population from the Alternate Identifier&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now that we have queried Populations based on the identifier, let’s display the resulting selection. Unlike reading the Population based on ID, though, the same alternative identifier could exist in multiple populations - meaning our response could return more than one population!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Cloning, Copying, and Pasting Nodes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To see this in action, we’ll display that result in a message like we did before. But rather than build a brand new node, right-click and &lt;strong&gt;Clone&lt;/strong&gt; the existing HTTP node with our population’s message. Cloning a node, or copy/pasting a selection of nodes (highlighted by holding and dragging your mouse), makes building larger DaVinci flows faster and easier.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Cloning a Node&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Wire the response of Read Population to the cloned node, and then update your cloned node to look at the &lt;code&gt;populations&lt;/code&gt; response from the query rather than a single population. &lt;strong&gt;An important note - the population value in your cloned node is still pointing to the original connector it was connected to (i.e. Read Population by ID). When cloning, make sure your references are matching the correct nodes.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Displaying all Populations&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;With this all connected, applied, and deployed, add an alternative identifier to a Population that matches an email domain you can test. In this example, I’m using “bxretail.org”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Setting the Alternative Identifier on the Population&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, when I try the flow and enter a user who doesn’t exist using that email domain, the population (or populations) with that alternative identifier show up.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Retrieving Populations on Email Domain&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Selecting a Population from a List&lt;/h3&gt;
&lt;p&gt;When the User does not exist and we query Populations based on the email domain, there are a series of outcomes we need to account for:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The email domain is associated to one Population&lt;/li&gt;
&lt;li&gt;The email domain is associated to more than one Population - in which we let the user select their Population&lt;/li&gt;
&lt;li&gt;The email domain isn’t associated with any Population - in which we fall back to the Default Population&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To do so, we will implement some &lt;strong&gt;Function Connectors&lt;/strong&gt; to branch our path. Function Connectors let us compare values and make decisions based on what we find, or run JavaScript to do things like modify or create values to pass into downstream connectors.&lt;/p&gt;
&lt;p&gt;Right-click and &lt;strong&gt;Delete&lt;/strong&gt; your HTTP node that displays populations. We won’t be needing it anymore.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Functions Connectors**&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In its place connect a “Functions” Connector and select the “A &amp;gt; B” Trigger, adding the title “Any Pops Found?”. Set the &lt;strong&gt;Type&lt;/strong&gt; to “Number”.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;A&lt;/strong&gt;, add the “rawResponse” field from your Read Populations node, hover over the field, and copy the handlebars reference that appears. It’ll look something like &lt;code&gt;{{local.68dcy1mtem.payload.output.rawResponse}}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Getting the Raw Response&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The triggers within the PingOne connector are making API calls behind the scenes, and if you know the &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/platform/populations/read-all-populations.html&quot;&gt;Read Populations&lt;/a&gt; request you’re aware that there’s an additional field that’s hidden in this result set: the &lt;strong&gt;size&lt;/strong&gt; - which informs you how many results have been returned from the request.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Accessing Hidden Fields&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Just because the field doesn’t show up in the schema selector doesn’t mean that the field is missing - it just means that the connector didn’t define that value in its output schema. To get access to size, modify your copied handlebars to include size as a key, something like &lt;code&gt;{{local.68dcy1mtem.payload.output.rawResponse.size}}&lt;/code&gt; and then copy/paste that change as Value A in your A &amp;gt; B node. Value B, then, should be set to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Accessing Hidden Fields&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; That seemingly arbitrary string of values after &lt;code&gt;local&lt;/code&gt; is your Node ID - and it’s how each node is being referenced throughout your flow. You can find the Node ID under the header of the Node editor or by toggling “Show Node IDs” in your flow settings (top-right menu).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We now have a True/False branch we can use. Connect an “All Triggers True” response to the Function Connector with the Trigger “A &amp;gt; B”, the &lt;strong&gt;Title&lt;/strong&gt; “More than One Pop Found?”, &lt;strong&gt;A&lt;/strong&gt; set to the handlebars you used in the prior node, &lt;strong&gt;B&lt;/strong&gt; set to 1, and the &lt;strong&gt;Type&lt;/strong&gt; set to Number.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Checking for Many Populations&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From that same Action Decision, wire an “Any Trigger False” to a PingOne Node with the Trigger “Read Population” and the selector set to “Default”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Getting the Default Pop When None are Found&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; HTML Templates and Custom CSS/JavaScript&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If more than one Population is found, we want the User to select their preferred Population. To do this, we are going to go back to the marketplace to insert the &lt;a href=&quot;https://marketplace.pingone.com/item/format-select-options-for-custom-html-node-group&quot;&gt;Dynamic Dropdown node&lt;/a&gt;. This node pack is a bit different than our forms as it uses the HTTP Connector’s Custom HTML Template to render content, which gives us a great opportunity to review a more complex node.&lt;/p&gt;
&lt;p&gt;First off, connect an “All Triggers True” trigger from the “More than One Pop Found?” node to the “Form Options Array” node that you just imported. Then, in that node you just connected, add the “populations” array gathered from your “Read Pop(s) by Alt ID” Node, setting the &lt;code&gt;optionLabelKey&lt;/code&gt; to &lt;code&gt;name&lt;/code&gt; and the &lt;code&gt;optionValueKey&lt;/code&gt; to &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Connecting the Populations List&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create a new Population (I named mine “Example 2”) with the same alternative identifier, and then Apply, Save, Deploy, and Try the Flow. When you enter in a new User with that domain, you should see a dropdown with the associated Populations. But what gives - the page looks terrible!&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Unformatted HTML Page&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The HTTP Node you imported lets you serve custom HTML, CSS, and JavaScript - it’s great in cases when you require heavier customization, interactivity, or interaction with an external CMS. In this case, our template that we imported relies on &lt;a href=&quot;https://marketplace.pingone.com/item/ping-ux-css&quot;&gt;Ping UX - DaVinci CSS&lt;/a&gt;: so let’s import that into the flow.&lt;/p&gt;
&lt;p&gt;To do so, click on the menu (three dots) in the top right corner and select “Flow Settings”, and then in the “Customizations” tab under “Page Customization” enable “Use Custom CSS”. There you can add in the Custom CSS Rules and the Custom CSS files provided in the Marketplace (or, for your own use cases, the custom assets you have from your own system).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding the Custom Ping CSS&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; Want a starting off point? Add the &lt;a href=&quot;https://marketplace.pingone.com/item/davinci-design-studio&quot;&gt;DaVinci Design Studio&lt;/a&gt; Chrome extension to get custom css that can be used in tandem with the Ping CSS provided.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Apply, Save, Deploy, and Try the Flow again. You’ll see the form shows up with the formatting matching the baseline branding in your form editor, with one exception: the Logo isn’t showing up.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Properly-Formatted Dropdown&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you read through the Dynamic Dropdown node, you’ll see that it also (optionally) expects the &lt;a href=&quot;https://marketplace.pingone.com/item/davinci-branding-variables-node&quot;&gt;DaVinci Branding Variables Node&lt;/a&gt;. This node lets you quickly and easily set the name of your company and the URL of your logo to be used across all HTTP nodes. Copy that node and place it at the beginning of your flow, connected to the first form in which you show the username.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Set Company &amp;amp; Logo Node&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You know the drill. Apply, Save, Deploy, Try. Now your logo shows up on the page too.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Input and Output Schema&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both the Functions Connector and the HTTP Connector we just added have an &lt;strong&gt;Input&lt;/strong&gt; and an &lt;strong&gt;Output schema.&lt;/strong&gt; These schemas exist so that you can add specific data requirements on what data is needed for this node to function and what data will be returned for use in downstream nodes.&lt;/p&gt;
&lt;p&gt;Let’s take a look at how the schemas are defined and what they look like in practice, starting with the “Form Options Array” Custom Function Node.&lt;/p&gt;
&lt;p&gt;Clicking into this node, you’ll see that some JavaScript is being executed to parse an array of objects and format that array into something that the HTML node can use when building the dropdown. At the top of the node you’ll see a &lt;strong&gt;Variable Input List&lt;/strong&gt; - this is where the variables that are being passed in the &lt;code&gt;params&lt;/code&gt; object you can see referenced in line 3 of the script are coming from. These variables are given a name, a value, and a data type, which helps inform the script how to parse and interact with that params value.&lt;/p&gt;
&lt;p&gt;Underneath the hood, these variables are being defined with an &lt;strong&gt;Input Schema&lt;/strong&gt; using &lt;a href=&quot;https://json-schema.org/&quot;&gt;JSON Schema&lt;/a&gt; formatting. In the case of the Custom Function Node, how those parameters are passed into the JavaScript is handled for you.&lt;/p&gt;
&lt;p&gt;At the other end of the JavaScript function, you’ll see that you’re returning an object with the key &lt;code&gt;options&lt;/code&gt;). This contains the key/value pairs that are being referenced in the HTML node. But while this data is being returned, the JavaScript by itself doesn’t inform the downstream nodes what the data looks like. The &lt;strong&gt;Output Schema&lt;/strong&gt; provides that definition.&lt;/p&gt;
&lt;p&gt;Scrolling to the bottom of the node definition, you’ll see an Output Schema section with the following JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;output&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;options&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;array&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;items&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          &quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;label&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;value&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          &quot;required&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;label&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;value&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll see that the &lt;strong&gt;output&lt;/strong&gt; of this node is expected to return a property of name &lt;strong&gt;options&lt;/strong&gt;, which is an &lt;strong&gt;array that&lt;/strong&gt; contains &lt;strong&gt;objects&lt;/strong&gt; whose properties are &lt;strong&gt;label&lt;/strong&gt; and &lt;strong&gt;value&lt;/strong&gt;: both strings.&lt;/p&gt;
&lt;p&gt;When you click on the “Dynamic Dropdown” HTTP Node, you’ll see that the &lt;strong&gt;options&lt;/strong&gt; array is passed and that you can reference it within the output of the Form Options Array node. Since the Label and Value were also defined, you can see that you can select that option directly from that object, to do things like target a specific element in the array.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Resulting Output&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since we are in the HTTP node, let’s take a look at how the inputs are formatted - since all of the input elements up to the HTML template are special to just this created node. Instead of an input editor like we saw in the Function Node, the Inputs are defined in an Input Schema halfway down the page - right next to the Output schema and just below the Form validation rules.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; If you are making your own HTML template, toggle the “Switch View” button to view the template using a classic code editor. When you’re adding variables, toggle back and use the “{}” button like you’ve done in other inputs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Input Schema here defines not only what values are expected to be available, but also how the inputs should be formatted for the user to fill in (&lt;code&gt;preferredControlType&lt;/code&gt;) and whether or not the user can dynamically pass parameters from other nodes (&lt;code&gt;enableParameters&lt;/code&gt;). The type and displayName indicate how the input is used and how it shows up for the user in this node. When editing the Input Schema, it’s important to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your inputs should be put within the &lt;code&gt;properties&lt;/code&gt; object.&lt;/li&gt;
&lt;li&gt;The only &lt;code&gt;preferredControlType&lt;/code&gt; available is &lt;code&gt;textField&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your inputs &lt;strong&gt;must use unique keys&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If you define a propertyName, &lt;strong&gt;the propertyName must match the key.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It’s best to edit the Input Schema in a separate text editor. It makes editing and validating the schema easier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Try adding another input - let’s call it &lt;code&gt;example&lt;/code&gt;. It should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;example&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;My New Example Input&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply your changes. You’ll see the new input ready to go with the key &lt;code&gt;example&lt;/code&gt; available for use in your HTML, CSS, and JavaScript via handlebars.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The New Input&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There’s no need to keep that input. You can remove or leave it in, your call.&lt;/p&gt;
&lt;p&gt;The Input and Output Schema shows up throughout DaVinci - you’ll see it in Teleports (we’ll get to that later) and the overall Input Schema for your flow. These definitions allow you to strongly define what is being passed in and out of each node, from one flow to another, or from another application into the DaVinci flow.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Output Fields&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While looking at the HTTP Node you probably noticed that there was nothing defined in the Output Schema but that node needs to return some data to the next node - in this case, what was selected and what button was pressed. Custom HTML Templates reference output data defined directly in the HTML itself and use the Output Fields list to link that data back to the node’s output.&lt;/p&gt;
&lt;p&gt;In the Output Fields List section you should see two properties: one called &lt;code&gt;select&lt;/code&gt; and one called &lt;code&gt;buttonValue&lt;/code&gt;. Now, take a look in the HTML Template: you’ll see that our select has the &lt;code&gt;id&lt;/code&gt; of &lt;code&gt;select&lt;/code&gt; and our submit button has the &lt;code&gt;data-skbuttontype&lt;/code&gt; of &lt;code&gt;form-submit&lt;/code&gt; and the &lt;code&gt;data-skbuttonvalue&lt;/code&gt; of &lt;code&gt;CONTINUE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For standard inputs, the &lt;code&gt;id&lt;/code&gt; of that input is referenced in the Output Fields List. For buttons, that definition is handled by the &lt;code&gt;sk&lt;/code&gt; attributes that live on it - in particular, that &lt;code&gt;skbuttonValue&lt;/code&gt; is holding the value that you can reference later, and that “buttonValue” output field is referring to it.&lt;/p&gt;
&lt;p&gt;For a list of SK Attributes, head to the documentation &lt;a href=&quot;https://docs.pingidentity.com/davinci/flows/davinci_sk_attributes.html&quot;&gt;here&lt;/a&gt;. For a list of standard components that can be added to HTML (called SK-Components), such as polling, camera, recaptcha, and filepickers, go &lt;a href=&quot;https://docs.pingidentity.com/davinci/flows/davinci_sk_components.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You’ve also probably noticed that both the button and the select have a Data Type of String and a Control Type of Text Field. For all intents and purposes, those two fields should be left as-is: the conversion from your HTML to the other options may not work as expected.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; When working with forms, make sure that the &lt;code&gt;id&lt;/code&gt; of your form matches the &lt;code&gt;data-skform&lt;/code&gt; html tag on your submission buttons - otherwise, your DaVinci flow won’t receive the results of your form data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Changing the Theme based on the Population&lt;/h1&gt;
&lt;p&gt;You now have a means to identify the Population based on an existing User or a new User with the added capability for the User to select their Population if their username matches more than one. Let’s cater the experience based on the context that their Population provides.&lt;/p&gt;
&lt;p&gt;Each Population can be assigned its own Theme. This Theme can then be referenced within DaVinci to stylize your user’s experiences.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; Teleports&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We have two different experiences that we will want to present to our user, a &lt;strong&gt;Login&lt;/strong&gt; experience and a &lt;strong&gt;Registration&lt;/strong&gt; experience, and more than one place in which we need to surface it. In the case of Registration, for instance, we both will need to perform the registration process if a population was selected by the user, if there was only one population found, or if the default population was selected instead. You could duplicate functionality, but if you want to change something later you’re going to have to find and change each implementation one at a time, which could lead to some pretty hard to troubleshoot errors. You could wire up each node to the same path, but not only does that get visually confusing you’ll have to handle all of the different outputs per node. This is where &lt;strong&gt;Teleports&lt;/strong&gt; come in.&lt;/p&gt;
&lt;p&gt;From a coding sense, think of a DaVinci flow as a &lt;strong&gt;module&lt;/strong&gt;. It likely performs a series of things in combination with each other to enact a larger action - like registration and login. Within that module are &lt;strong&gt;functions&lt;/strong&gt; - little bits of smaller atomic actions that are invoked for the module to do its thing. &lt;strong&gt;Teleports&lt;/strong&gt; are these functions. Teleports allow you to break out smaller pieces of functionality into reusable segments that can be called on, looped from, and reused without any copy/pasting or complex wiring.&lt;/p&gt;
&lt;p&gt;As a rule of thumb, if you are reusing the same nodes over and over again, consider putting them in a Teleport.&lt;/p&gt;
&lt;p&gt;To start, scroll below the flow you’ve built so far and add an annotation to indicate what this teleport is for (I’m using “Login”). It’s best practice to write flows horizontally and individual teleports vertically, ideally with the teleports in execution order (for example, if you have a teleport that returns a session or an error, it should probably be at the bottom) - that way, you can read through the content of a flow similar to how you would read a flowchart or diagram.&lt;/p&gt;
&lt;p&gt;Next, add in a Teleport connector with the trigger “Define a Start Node”. Once you do, you’ll see something pretty familiar - an Input schema! Teleport Start Nodes tell the flow where a new section begins and what data can be passed into that section: think of it like a function definition.&lt;/p&gt;
&lt;p&gt;First off, change the name of your Teleport in the Settings tab - you’ll use that name to identify the Teleport later. I’m naming mine “Login”.&lt;/p&gt;
&lt;p&gt;The Input Schema should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;p1UserId&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;PingOne User ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;p1UserId&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;p1UserName&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;PingOne Username&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;p1UserName&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;populationId&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;populationId&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;populationThemeId&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;populationThemeId&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population Theme ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;populationIdPId&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;populationIdPId&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population IdP ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to pass our PingOne User’s ID and Population Details into the Teleport.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; Teleports stringify the data that comes into it and as a result, won’t be able to handle nested object definitions. Either define your required fields individually in the input schema, set them in a Flow Instance Variable, or write a custom function to parse the values from the stringified object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In our case, since we only need two values from our Population it’s easiest to define them at the top level.&lt;/p&gt;
&lt;p&gt;Now connect a Form Connector with the Show Form Trigger to your Teleport Start Node, selecting the “Password Authentication” form. Change the Form Theme to “Use Theme ID” and pass in the Theme ID from your parsed Population.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Passing Parsed Object Variables into a Form&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You probably noticed that your variables list looked different inside this new section. That’s because the Teleport only has access to the data that was passed into it or variables set at the Global or Flow Instance level. Just like a function, the teleport can only see its parameters, top level constants, or run-time variables.&lt;/p&gt;
&lt;p&gt;Back up to your Read Population by ID node (the one called after you discover an existing user), replace your HTTP node with a Teleport but this time use the &lt;strong&gt;Go to Start Node&lt;/strong&gt; trigger and select your Login teleport as the start node.&lt;/p&gt;
&lt;p&gt;Your variables for a PingOne User ID and Population Details will appear. Add in your PingOne User from your Find User node and the PingOne Population Details from the Find Population node.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Go to Teleport&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You’ve now set up a way to pass data into a teleport - basically, execute a function in your module. Apply, Save, Deploy, and Try the flow with a user that does exist. You should now hit the Password page.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Password Page&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But the theme looks just like the last page! Let’s change that.&lt;/p&gt;
&lt;p&gt;Back in PingOne, under User Experience → Branding and Themes, go ahead and create a new theme. I’m using the base “Slate” theme for this example.&lt;/p&gt;
&lt;p&gt;When you’re feeling good about the theme, under Directory → Populations select your Population and under Configuration assign your new theme.&lt;/p&gt;
&lt;p&gt;The next time you go through the flow, you’ll see the theme change to your selected theme.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Updated Theme&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Enforcing Password Policies based on the Population&lt;/h1&gt;
&lt;p&gt;You can set a unique Password Policy per Population so that each User segment can meet the security requirements set by their organization.&lt;/p&gt;
&lt;p&gt;Since a User can only be a part of a single Population, the default node already knows their password policy during login. From the Password Authentication Node, connect a PingOne Connector with a Check Password Trigger, adding the teleported p1UserId and the password you collected from the form.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Check Password Node&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A password can be in more than one state - it’s valid, invalid, or it needs to be updated in some way (e.g. it’s expired, doesn’t meet the new password policy requirements). If you wanted to just check if the password is/isn’t valid, creating responses using the “All Triggers True” and “Any Trigger False” responses works just fine. But, if you’d like to perform additional branches on validation you can use an A == B (Multiple Conditions) Function Node to compare the &lt;code&gt;status&lt;/code&gt; of the Check Password response to the response options shown in the &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/platform/users/user-passwords/password-check.html&quot;&gt;Check Password API reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For the sake of this example, we’ll create an outcome only on success - the error response will be handled for us.&lt;/p&gt;
&lt;p&gt;Wire up the Check Password Node to an HTTP connector with the Custom HTML Message trigger and give it the message “Password Status:” including the &lt;code&gt;status&lt;/code&gt; variable from your Check Password node. If you want to see all status information, set the path to be “Any Trigger Completes”.&lt;/p&gt;
&lt;p&gt;Now, when you log in with the correct password you’ll see the status information that you’d likely branch from in a more robust flow.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Returned Password Check Status&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Enforcing Login with an External IdP based on the Population&lt;/h1&gt;
&lt;p&gt;Some of your business groups and partnerships may have their own established Identity Provider that they’ll want to bring rather than having their users create and manage a separate account. Populations let you assign a default provider that, when a user federates with it, will associate them with the right Population (and as such the appropriate theming and managed administration).&lt;/p&gt;
&lt;p&gt;Your teleport has already been configured to pull in the external IdP ID tied to your population - we’ll just need to add the branching path that decides whether to log in with that IdP instead of locally.&lt;/p&gt;
&lt;p&gt;Connect your Teleport Start Node to a Function Connector with the “A is Empty” trigger and enable “Check undefined/null”. Connect the “All Triggers True” outcome to the Password Authentication node you made in the last section.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Check for Local Login&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, connect the “Any Trigger False” outcome to a PingOne Authentication Connector with the trigger “Sign On with External Identity Provider”, using the &lt;code&gt;populationIdPId&lt;/code&gt;, the &lt;code&gt;populationId&lt;/code&gt;, and the &lt;code&gt;p1UserName&lt;/code&gt; (in the Login Hint) you collected in the teleport.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; If you don’t want users to be able to link to an existing account or create a new account from an external IdP, uncheck the “Link with PingOne User” checkbox.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;External IdP Login&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To test, add an HTTP Connector with a custom HTML Message of your choosing - I’ve added the &lt;code&gt;statusCode&lt;/code&gt; from the external IdP Login. When you Log in with a Population that doesn’t have an External IdP configured, you should see the password page as you did before. But in the case you have an External IdP configured, you’ll redirect to their login page and then come back with your login status.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A Successful External IdP Login&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Dynamic Registration&lt;/h1&gt;
&lt;p&gt;We’ve created the Teleport for Logging in, but we haven’t handled the users who don’t exist yet. Hold and drag with your mouse to highlight all of your login teleport nodes and Duplicate the selection. Next:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rename the Annotation and the Teleport Start node to “Registration”.&lt;/li&gt;
&lt;li&gt;In the Teleport Start Node, remove &lt;code&gt;p1UserId&lt;/code&gt; since a user doesn’t exist yet.&lt;/li&gt;
&lt;li&gt;Change the Password Authentication form to “Example - Registration” and pass in the &lt;code&gt;p1UserName&lt;/code&gt; into the username and email field values.&lt;/li&gt;
&lt;li&gt;Change the Check Password Trigger to the Create User Trigger and pass in the Username, Email, Password, and Population ID.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, wire up the following connections in your New User branch at the top of your flow to the Go To Teleport for Registration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using an “All Triggers True” after the “Get Default Pop” node, using the population details from the Get Default Pop and the Username from the Username form.&lt;/li&gt;
&lt;li&gt;Using an “Any Triggers False” after the “More than One Pop Found?” node, using the top Population result from the “Read Pop(s) by Alt Id” node and the Username from the Username form.&lt;/li&gt;
&lt;li&gt;Connecting from the Dynamic Dropdown node (replacing the HTTP node), connect a PingOne Connector with a Find Population trigger using the selected population ID, and then wire the true outcome to the Registration teleport using the newfound population details. Alternatively, replace the Get Population node with a Custom Function that returns the appropriate population details from the previous call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, when you enter a new user you are taken to the appropriate registration page depending on your Population preferences, automatically using the appropriate password policy and external IdP.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;New User Registration&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Connecting the Flow to Your Application(s)&lt;/h1&gt;
&lt;p&gt;Up to this point we have used the “Try Flow” button to test out our DaVinci flow. While this approach is great for testing or for flows that execute actions without the need for returning user context, in the real world it’s highly likely that your flows will complete with a user logging into an Application with an active session from PingOne.&lt;/p&gt;
&lt;p&gt;To start, let’s make two more Teleports: one to return an authentication success and the other to return a failure.&lt;/p&gt;
&lt;p&gt;Duplicate just the annotation and the Start Teleport Node from your Login section and then change the Input Schema for your Login Success Teleport to only have the &lt;code&gt;p1UserId&lt;/code&gt; - that’s all we need to authenticate the user. Name the new section and teleport “Login Success”.&lt;/p&gt;
&lt;p&gt;From that node connect a PingOne Authentication Connector with the trigger “Return Success Response (Redirect Flows)”. Configure this node to use the User ID from the Teleport and leave the Authentication Method as “pwd” since we aren’t performing any additional form of risk or multifactor-based authentication. You’ll see in this node you can optionally decorate the ID and Access Tokens with custom claims as needed by your downstream application.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Success Route&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Duplicate the nodes and annotations, renaming the new section and teleport “Login Failure”. Inside the teleport, change the input schema to capture the Error Message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;errorMessage&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Error Message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;errorMessage&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;errorDescription&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Error Description&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;errorDescription&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;errorReason&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Error Reason&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;preferredControlType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;textField&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;enableParameters&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;errorReason&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, change the PingOne Authentication Connector’s trigger to “Return Error Response” with a custom error message that uses the inputs you just provided in the Teleport’s Input Schema (I set the Error Message to &lt;code&gt;invalid_request&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Success and Failure Teleports&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now that you have a means to inform the downstream applications of a success or failure, let’s wire it up to the appropriate paths in the flow.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From Node&lt;/th&gt;
&lt;th&gt;Connect Go To Teleport&lt;/th&gt;
&lt;th&gt;With Variables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Check Password, Inside Login&lt;/td&gt;
&lt;td&gt;Login Success (All True)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;p1UserId&lt;/code&gt; from the teleport, or the &lt;code&gt;id&lt;/code&gt; returned from Check Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check Password, Inside Login&lt;/td&gt;
&lt;td&gt;Login Failure (Any False)&lt;/td&gt;
&lt;td&gt;The error details from Check Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External IdP Node, Inside Login&lt;/td&gt;
&lt;td&gt;Login Success (All True)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;p1UserId&lt;/code&gt; from the teleport, or the &lt;code&gt;id&lt;/code&gt; returned from External IdP Login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External IdP Node, Inside Login&lt;/td&gt;
&lt;td&gt;Login Failure (Any False)&lt;/td&gt;
&lt;td&gt;The error details from External IdP Node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create User, Inside Registration&lt;/td&gt;
&lt;td&gt;Login Success (All True)&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;id&lt;/code&gt; returned from Create User&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create User, Inside Registration&lt;/td&gt;
&lt;td&gt;Login Failure (Any False)&lt;/td&gt;
&lt;td&gt;The error details from Create User&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External IdP Node, Inside Registration&lt;/td&gt;
&lt;td&gt;Login Success (All True)&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;id&lt;/code&gt; returned from the External IdP Login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External IdP Node, Inside Registration&lt;/td&gt;
&lt;td&gt;Login Failure (Any False)&lt;/td&gt;
&lt;td&gt;The error details from External IdP Node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Connected Teleports&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Learning:&lt;/strong&gt; PingOne Flows&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In order for our DaVinci flow to be connected to an application in PingOne, we will need to make it a PingOne Flow.&lt;/p&gt;
&lt;p&gt;PingOne operates with DaVinci via an OIDC redirect. When a user attempts to log in to an application in PingOne, and DaVinci is protecting that application (via a &lt;strong&gt;Flow Policy&lt;/strong&gt;), PingOne will redirect the user to DaVinci to perform its actions upon which it redirects back to PingOne to generate a user session and log the user in. The handoff looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;DaVinci with PingOne&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Underneath your Flow Settings (click the three dots in the top-right, select Flow Settings and under the General tab) enable PingOne Flow. This converts the flow to run for either OIDC or SAML authentication with PingOne.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Enabling a PingOne Flow&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once you do this (and deploy!) you’ll notice that a little “P1” icon appears next to your title and if you try to use the “Try Flow” button you’ll be redirected to an error page that indicates that this flow requires PingOne to run.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Hitting “Try Flow” with a PingOne Flow&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;DaVinci connects flows to PingOne applications using Flow Policies. Flow Policies let us define what flows and flow versions run, when they run, and what statistics we’d like to gather when users are driven through the flows (say, a successful outcome or a failure).&lt;/p&gt;
&lt;p&gt;On the DaVinci sidebar, click on “Applications”. You should see a page that includes a PingOne SSO Connection. You can add more Flow Policies to the existing PingOne Connection or create a new one - this lets you create bundles of Policy categories, making it easier to identify when selecting the Policy in PingOne.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;DaVinci Applications&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create “Add Application” and name it “Dynamic Populations”, then click on the created Application. You’ll see that this application is its own OIDC client with a unique client ID and client secret, and that the “Company ID” is actually the ID of the PingOne environment that this instance of DaVinci is tied to. This lets you take that same application and use it with non-PingOne systems you want DaVinci to interact with.&lt;/p&gt;
&lt;p&gt;Select the “Flow Policy” tab and click on “Add Flow Policy”. &lt;strong&gt;Make sure you add a name in the first step&lt;/strong&gt; (in my case I’m doing “Population Login and Registration”) and select “PingOne Flow Policy” since we will be interacting with a PingOne application. An option to Bypass flows will appear - skip that section and click Next.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding a Flow Policy - Step 1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You’ll now see a picker of PingOne-configured flows that you have created. Select the flow you’ve been working on (Dynamic Experiences with Populations) and click on “Latest Version”.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; DaVinci holds up to 100 past versions of your deployed flows - meaning that you can “lock” an application down to a specific version of a flow while you develop a new one without impacting your users’ experience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding a Flow Policy - Step 2&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, set your selected flow to 100% distribution and set the PingOne Authentication (Login Success) node as your Success Node.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⭐ DaVinci Tip:&lt;/strong&gt; DaVinci Flow Policies let you select multiple flows and flow versions in the same policy and then set a distribution to each instance within that policy. That, coupled with the success node definitions, allows you to A/B test and gather feedback on different approaches with your users simultaneously.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding a Flow Policy - Step 3&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At this point you have defined an Application and a Flow Policy within that Application. Let’s assign that policy to PingOne to be evaluated when a user attempts to login.&lt;/p&gt;
&lt;p&gt;Back in PingOne, click on the “Applications” tab and submenu. Feel free to use a custom SAML or OIDC application, but for the sake of simplicity I’m going to use the “PingOne Self-Service - MyAccount” application.&lt;/p&gt;
&lt;p&gt;Clicking on your application, select the “Policies” tab and click “Add Policies”. You’ll see a DaVinci tab in the subsequent editor - click on that tab and then check the new policy you just made.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Assigning a Flow Policy to a PingOne Application&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;By assigning this policy you have informed PingOne that for a user to successfully authenticate into this application they are required to first go through the DaVinci flow you created.&lt;/p&gt;
&lt;p&gt;On the Overview tab, click on the Home Page URL of the Self Service Application (or in the case of your OIDC/SAML App, the appropriate calling URL) and open in an incognito tab. You’ll be greeted with a familiar sight - your DaVinci flow! Successfully complete login and you’ll be redirected to your My Account page (or wherever your app completes).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A Logged-In User&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;You now have a way to dynamically branch login and registration experiences based on contextual data supplied by the User and Population. You’ve also built a toolbox of relevant skills in DaVinci to build more complex, interesting, and incredible flows confidently.&lt;/p&gt;
&lt;p&gt;DaVinci is an incredibly powerful tool, and coupling it with the strong capabilities and services within PingOne you can design the most secure and seamless experiences for your users.&lt;/p&gt;
&lt;p&gt;For a copy of the completed flow, go &lt;a href=&quot;https://github.com/gwizdala/lib-ping/blob/main/How-Tos/davinci-primer-dynamic-populations/dynamic_experiences_with_populations.json&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Other Helpful Tricks&lt;/h1&gt;
&lt;p&gt;There are some other capabilities in PingOne that may prove useful as you work your way through more complicated flows.&lt;/p&gt;
&lt;h2&gt;Variables&lt;/h2&gt;
&lt;p&gt;Variables allow us to set data that can either be stored and referenced at a particular instance of a running flow (that’s a Flow Instance Variable, like a module-level constant) or globally across all executions (that’s a Global Variable or Company Context, and it’s exactly what you think).&lt;/p&gt;
&lt;p&gt;You can set variables within nodes in Flows (you actually did that in the “Set Company &amp;amp; Logo” node) as well as via the “Variables” tab on the left menu. It’s likely that you’ll set Flow Instance Variables dynamically within a Flow and Global Variables from within the menu (as those are normally static for all flow executions). A great example use case of Global Variables is the DNA Flows mentioned before - check out their &lt;a href=&quot;https://github.com/pingone-davinci/davinci-dna-flows/blob/main/DV-VariableObject-ConfigurationGuide.md&quot;&gt;configuration guide&lt;/a&gt; for an example of what you can store.&lt;/p&gt;
&lt;h2&gt;Parsing Objects from a Teleport&lt;/h2&gt;
&lt;p&gt;If you had a complex object that you wanted to pass directly into a teleport, it’s useful to parse that object in a subsequent Custom Function Connector.&lt;/p&gt;
&lt;p&gt;The function within that connector should be incredibly simple - return the name of the object you want to return and parse the passed object into JSON.&lt;/p&gt;
&lt;p&gt;As an example, if you wanted to pass the Population object into a teleport directly you could create a script that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;}) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&apos;population&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(params.populationString)}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a production use case you may want to add some parametrization and checking to validate that your object has all of the values you’re looking for.&lt;/p&gt;
&lt;p&gt;Next, set your Output Schema to map to the object you want to reference.&lt;/p&gt;
&lt;p&gt;Using the same use case, your output schema may look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;output&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; 			&quot;population&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;population&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;id&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;name&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Population Name&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;preferredLanguage&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Preferred Language&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;preferredLanguage&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;passwordPolicy&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Password Policy&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;passwordPolicy&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							&quot;id&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Password Policy ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;theme&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Theme&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;theme&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							&quot;id&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Theme ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					&quot;defaultIdentityProvider&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Default Identity Provider&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;defaultIdentityProvider&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							&quot;id&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Default Identity Provider ID&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							&quot;type&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;displayName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Default Identity Provider Type&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;								&quot;propertyName&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;							}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;						}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;					}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;				}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach gives us a parsed object we can reference in later nodes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.1PUsVrVE_Z16vVkj.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Introducing the DaVinci MCP Server</title><link>https://developer.pingidentity.com/blog/introducing-the-davinci-mcp-server/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/introducing-the-davinci-mcp-server/</guid><description>Introducing the DaVinci MCP Server: Connect AI assistants to your PingOne DaVinci environment for faster flow exploration, validation, and troubleshooting.</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As the market moved toward AI assistants and agent-based workflows, I kept coming back to one question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does this mean for identity orchestration platforms?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;More specifically, what should identity orchestration look like when AI assistants become part of the developer workflow?&lt;/p&gt;
&lt;p&gt;Identity journeys are not just pieces of code. They represent onboarding, authentication, MFA, recovery, fraud checks, and access decisions. They are the product of decisions made by architects, developers, product managers, and security teams, often over many months, sometimes by people who have since moved on. If AI assistants are going to help teams build and operate software, they also need a better way to understand the identity journeys behind those experiences.&lt;/p&gt;
&lt;p&gt;That realization came to me through a very ordinary scenario.&lt;/p&gt;
&lt;h2&gt;Life Before AI-Assisted Orchestration&lt;/h2&gt;
&lt;h3&gt;When a Simple Support Ticket Becomes an Identity Investigation&lt;/h3&gt;
&lt;p&gt;Silvia is a support agent at ACME Corp. She has received several tickets from users who cannot register their FIDO2 device during account creation. She knows that MFA device onboarding runs through DaVinci, but she does not know which flow is involved, where the failure is happening, or who to ask first.&lt;/p&gt;
&lt;p&gt;Traditionally, her path forward looks like this: escalate to the flow development team, wait for someone to pick up the ticket, have them identify which application and flow policy is in scope, inspect the connector configuration, review execution logs, and determine whether the failure is in the flow logic, the MFA connector, a policy decision, the user’s device, or something upstream in PingOne.&lt;/p&gt;
&lt;p&gt;The customer issue is simple to describe. The investigation is not.&lt;/p&gt;
&lt;p&gt;Silvia does not need to become a DaVinci expert. But she does need a faster way to ask operational questions: &lt;em&gt;Which flow handles this onboarding journey? Where does FIDO registration happen? Which connector is used? What did the last ten failed executions look like? What should I check first?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Right now, those questions require a specialist. They do not have to.&lt;/p&gt;
&lt;h2&gt;Need of the Hour&lt;/h2&gt;
&lt;p&gt;These are situations where the need for an MCP server becomes imminent, as a practical way to make DaVinci administration and troubleshooting easier.&lt;/p&gt;
&lt;p&gt;If an AI assistant can securely access the structure of a DaVinci environment, it can help people like Silvia understand flows faster, identify dependencies, validate configuration, and troubleshoot failures without starting from scratch every time.&lt;/p&gt;
&lt;h2&gt;A Quick Word on MCP&lt;/h2&gt;
&lt;p&gt;Before we go further, MCP, or the Model Context Protocol, is an open standard introduced by Anthropic that allows AI assistants to connect to external tools and data sources through a common interface. Instead of fragmented, custom integrations between AI tools and every system a team uses, MCP provides a standard protocol for connecting AI assistants to the systems &lt;strong&gt;where context actually lives&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For identity orchestration, that &lt;strong&gt;context&lt;/strong&gt; lives in flows, applications, connectors, variables, forms, flow versions, validation results, and execution history.&lt;/p&gt;
&lt;p&gt;MCP is the bridge. The DaVinci MCP Server is what sits on the other side of it.&lt;/p&gt;
&lt;p&gt;For a deeper look at MCP’s architecture, workflow, and security model, see the &lt;a href=&quot;https://developer.pingidentity.com/identity-for-ai/agents/idai-what-is-mcp.html&quot;&gt;Ping Identity MCP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The DaVinci MCP Server&lt;/h2&gt;
&lt;p&gt;Today we are introducing the &lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/davinci-mcp-server/overview.html&quot;&gt;&lt;strong&gt;DaVinci MCP Server&lt;/strong&gt;&lt;/a&gt;, an open-source MCP server that gives MCP-compatible AI assistants secure, structured access to your DaVinci environment.&lt;/p&gt;
&lt;p&gt;This is our first step toward making DaVinci context available to AI. The current release focuses entirely on &lt;strong&gt;read access&lt;/strong&gt;; exploration, inspection, validation, and execution monitoring. The goal for this first release is to help teams understand their DaVinci environments faster, and then gradually move towards automating changes to DaVinci resources.&lt;/p&gt;
&lt;p&gt;The MCP server includes tools organized into two collections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;davinci_admin:&lt;/strong&gt; Core administrative tools for exploring your environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List and inspect flows, flow versions, and full flow definitions including node graphs&lt;/li&gt;
&lt;li&gt;List applications and application flow policies&lt;/li&gt;
&lt;li&gt;Discover available connectors and deployed connector instances&lt;/li&gt;
&lt;li&gt;Get details on variables and forms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;davinci_flow_troubleshooting:&lt;/strong&gt; Specialized tools for validation and debugging flow configurations and troubleshooting flow executions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Life After AI-Assisted Orchestration&lt;/h2&gt;
&lt;p&gt;The first time this becomes useful is not some dramatic moment where an AI agent fixes an entire identity journey by itself.&lt;/p&gt;
&lt;p&gt;It is a moment where support agents like Silvia can directly ask, &lt;em&gt;“Where are users failing during FIDO registration?”&lt;/em&gt; and getting a place to start instead of an escalation queue.&lt;/p&gt;
&lt;p&gt;It is an architect asking, &lt;em&gt;“Is this flow ready to deploy?”&lt;/em&gt; and seeing validation errors and warnings explained in plain language, not buried in a UI they have to navigate manually.&lt;/p&gt;
&lt;p&gt;The exact time savings will vary by environment, flow complexity, and familiarity with DaVinci. But the difference in the &lt;strong&gt;starting point&lt;/strong&gt; is consistent. With the DaVinci MCP Server, an AI assistant can surface the relevant flows, dependencies, validation results, and execution details at the start of the conversation, not at the end of a multi-hour investigation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Persona&lt;/th&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Without DaVinci MCP Server&lt;/th&gt;
&lt;th&gt;With DaVinci MCP Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New developer / architect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherited an onboarding journey, needs to improve drop-off rates&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Days to weeks:&lt;/strong&gt; learn DaVinci concepts, inspect flows and identify dependencies manually.&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hours&lt;/strong&gt;: ask the assistant to explain the flow, identify dependencies in business terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Support / operations team&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Production issue reported, team does not know which flow or node is responsible&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hours or days&lt;/strong&gt; of back-and-forth between support and identity admins, starting with no context&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Faster first response&lt;/strong&gt;: provide user and approximate timelines and get details on point of failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Identity architect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Review whether a flow is production-ready&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hours&lt;/strong&gt; of manual review across the flow ecosystem, and known best practices&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Minutes&lt;/strong&gt;: the assistant validates the flow configs and highlights blockers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Make changes to a flow&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hours&lt;/strong&gt; of manual inspection of the flow and its references&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Minutes&lt;/strong&gt;: the assistant generates a flow dependency summary before the developer touches anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Product manager / business owner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Onboarding conversion dropped, wants to understand why&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Days or weeks&lt;/strong&gt;: Wait for technical teams to manually explain flows and correlate behavior&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hours&lt;/strong&gt;: Get understanding of the onboarding journey in business terms and ask more focused follow-up questions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Once teams see how much time is spent on context discovery and first-triage work, the natural next question becomes: which of these workflows can we automate? The read-only tools in this release are the foundation, but once you understand what your AI assistant can see and reason about, you start seeing which repetitive, manual, and rule-based investigation tasks are candidates for automation.&lt;/p&gt;
&lt;p&gt;The key takeaways are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start with simple things,&lt;/li&gt;
&lt;li&gt;build confidence in how AI interacts with your DaVinci environment,&lt;/li&gt;
&lt;li&gt;and progressively hand off more of the first-response and review work to AI-assisted or fully automated workflows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;The DaVinci MCP Server runs locally on your machine. Before you begin, make sure you have:&lt;/p&gt;
&lt;h3&gt;Local Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Node.js v22.0.0 or higher (&lt;a href=&quot;https://nodejs.org/en/download/package-manager&quot;&gt;Download &amp;amp; Install&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client such as Claude Code, VS Code with Cline, Cursor, Gemini CLI, or Claude Desktop&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PingOne Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A PingOne subscription (licensed or &lt;a href=&quot;https://www.pingidentity.com/en/try-ping.html&quot;&gt;free trial&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;DaVinci enabled on your PingOne environment (Admin Console → Environments → Services → DaVinci)&lt;/li&gt;
&lt;li&gt;A PingOne Worker Application configured with:
&lt;ul&gt;
&lt;li&gt;Authorization Code + PKCE&lt;/li&gt;
&lt;li&gt;redirect URI &lt;code&gt;http://127.0.0.1:7474/callback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Token Endpoint Authentication Method set to None&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A PingOne user with at least &lt;strong&gt;DaVinci Admin Read Only&lt;/strong&gt; role assigned&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Configuration Values You’ll Need from PingOne Admin Console&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Where to find it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAVINCI_MCP_ENVIRONMENT_ID&lt;/td&gt;
&lt;td&gt;Settings → Environment Properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTHORIZATION_CODE_CLIENT_ID&lt;/td&gt;
&lt;td&gt;Applications → your Worker App → Overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROOT_DOMAIN&lt;/td&gt;
&lt;td&gt;pingone.com (NA), pingone.eu (EU), pingone.asia/pingone.au (APAC), or your custom domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Add the Server to Your AI Client&lt;/h3&gt;
&lt;p&gt;For Claude Code CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;claude&lt;/span&gt;&lt;span&gt; mcp&lt;/span&gt;&lt;span&gt; add&lt;/span&gt;&lt;span&gt; --transport&lt;/span&gt;&lt;span&gt; stdio&lt;/span&gt;&lt;span&gt; davinci&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --env&lt;/span&gt;&lt;span&gt; DAVINCI_MCP_ENVIRONMENT_ID=&quot;your-environment-id&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --env&lt;/span&gt;&lt;span&gt; AUTHORIZATION_CODE_CLIENT_ID=&quot;your-client-id&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --env&lt;/span&gt;&lt;span&gt; ROOT_DOMAIN=&quot;pingone.com&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --&lt;/span&gt;&lt;span&gt; npx&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; @ping-identity/davinci-mcp-server&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For other clients (Claude Desktop, Cursor, VS Code Cline, Gemini CLI), add the equivalent JSON block to your MCP client configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;davinci&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;-y&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;@ping-identity/davinci-mcp-server&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;start&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;env&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;DAVINCI_MCP_ENVIRONMENT_ID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;your-environment-id&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;AUTHORIZATION_CODE_CLIENT_ID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;your-client-id&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;ROOT_DOMAIN&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;pingone.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VS Code and Cursor users can also use the one-click install deeplinks in the &lt;a href=&quot;https://github.com/pingidentity/davinci-mcp-server&quot;&gt;GitHub repository&lt;/a&gt;. For advanced configuration, filtering which tools or collections are exposed to the AI assistant, see the &lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/davinci-mcp-server/overview.html&quot;&gt;full setup guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Security&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.0 with PKCE&lt;/strong&gt; Every tool call is authenticated using OAuth 2.0 Authorization Code flow with PKCE. Your password is never stored. The resulting access token is saved in your OS-native keychain, the same place your browser stores credentials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read-only operations&lt;/strong&gt; The current release is entirely read-only. No flow mutations, no deployments, no configuration changes are possible through these tools. There is nothing to audit because nothing is written.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Least privilege&lt;/strong&gt; The agent has the same permissions as your authenticated PingOne user. Use the &lt;strong&gt;DaVinci Admin Read Only&lt;/strong&gt; role. Do not authenticate with an account that has write or admin-level access unless you have a specific reason and have reviewed the implications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trust your client&lt;/strong&gt; Only run this server with AI clients you trust. Review agent prompts and tool calls before deploying AI-assisted workflows to production.&lt;/p&gt;
&lt;h2&gt;What to Do Next&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Try it in a real scenario.&lt;/strong&gt; Think about something you or a teammate spent too long on recently, understanding a flow, diagnosing a failure, preparing for a review. Try it again with the MCP server connected to an AI assistant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Think about your workflows.&lt;/strong&gt; Where does your team spend time rediscovering context? Where are the first-triage steps that are repetitive and rule-based? Those are the candidates for AI-assisted or eventually automated workflows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share what you find.&lt;/strong&gt; The Ping Identity developer community is where use cases get refined. What worked, what did not, what you wish the server could do, that feedback shapes the roadmap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raise issues on the repository.&lt;/strong&gt; If you hit a bug, a gap, or if you have an enhancement request &lt;a href=&quot;https://github.com/pingidentity/davinci-mcp-server/issues&quot;&gt;open an issue&lt;/a&gt;. We are listening.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pingidentity/davinci-mcp-server&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@ping-identity/davinci-mcp-server&quot;&gt;NPM Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/davinci-mcp-server/overview.html&quot;&gt;PingOne DaVinci MCP Server Developer Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/davinci/release_notes/davinci_release_notes.html&quot;&gt;PingOne DaVinci Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25&quot;&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc7636&quot;&gt;OAuth 2.0 PKCE - RFC 7636&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/blog/introducing-the-pingone-mcp-server/&quot;&gt;PingOne MCP Server Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/blog/introducing-the-aic-mcp-server/&quot;&gt;PingOne AIC Server Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.C_tWyq_a_1NWkw5.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Delegated Administration in PingOne</title><link>https://developer.pingidentity.com/blog/delegated-administration-in-pingone/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/delegated-administration-in-pingone/</guid><description>Enable your administrators, customers, and partners to manage their data, config, and users within PingOne</description><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/delegated-administration-in-pingone/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;Your business is likely not just one set of users. In today’s world, identity stretches beyond the borders of siloed Internal (Workforce Employee) or External (Customer) user engagement and relationships. And as that line blurs, so does the process of user administration.&lt;/p&gt;
&lt;p&gt;PingOne, Ping’s Multi-Tenant SaaS solution, provides a robust means to delegate and manage administrative capabilities across the platform.&lt;/p&gt;
&lt;p&gt;This Guide will teach you how you can create and manage delegated administration of Policies and Users within the PingOne Platform. It is broken down into the following parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#definitions&quot;&gt;Defining Key PingOne concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#delegated-administration&quot;&gt;Delegating Administration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extending-delegated-administration&quot;&gt;Extending Delegated Administration&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By the end of this Guide you’ll have configured multiple Environments with their own Applications, Populations, Groups, and Users. Additionally, you’ll have assigned Delegated Administrators to oversee capabilities within these Environments.&lt;/p&gt;
&lt;p&gt;This Guide expects a beginner level of familiarity with the PingOne administrative console. If you haven’t set one up yet, be sure to get access to a PingOne instance (register for a trial &lt;a href=&quot;https://www.pingidentity.com/en/try-ping.html&quot;&gt;here&lt;/a&gt;) so that you can follow along.&lt;/p&gt;
&lt;p&gt;But before we get into the details, let’s go over some quick platform definitions.&lt;/p&gt;
&lt;h1&gt;Definitions&lt;/h1&gt;
&lt;p&gt;Delegated Administration in PingOne will primarily interact with &lt;strong&gt;Environments&lt;/strong&gt;, &lt;strong&gt;Populations&lt;/strong&gt;, &lt;strong&gt;Groups&lt;/strong&gt;, and &lt;strong&gt;Admin Roles&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;An &lt;a href=&quot;https://docs.pingidentity.com/pingone/settings/p1_environments.html&quot;&gt;&lt;strong&gt;Environment&lt;/strong&gt;&lt;/a&gt; is a subdivision of your instance of PingOne. It contains the core resources you’ll use to build your IAM services, including your Users, the Applications they’ll single-sign-on (SSO) into, and the Services and Policies you’ll use to secure the access activities of those Users.&lt;/p&gt;
&lt;p&gt;A &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_populations.html&quot;&gt;&lt;strong&gt;Population&lt;/strong&gt;&lt;/a&gt; is a unique administrative set of Users and Groups. Populations contain their own configuration such as their own Password Policies, Branding, default Identity Provider, and/or “alternative identifiers” used to easily look up a Population in things like REST APIs and DaVinci Flows.&lt;/p&gt;
&lt;p&gt;A &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_groups.html&quot;&gt;&lt;strong&gt;Group&lt;/strong&gt;&lt;/a&gt; is a collection of Users. Groups can both be &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_groups_vs_populations.html#p1-static-dynamic-groups&quot;&gt;assigned dynamically and statically&lt;/a&gt; and can be &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_groups_vs_populations.html#p1-nested-groups&quot;&gt;nested inside one another&lt;/a&gt;. Groups are used as a way to assign permissions and membership for Users, including Application access and Admin Roles.&lt;/p&gt;
&lt;p&gt;Groups can be created one of two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;External Groups&lt;/strong&gt; come from an external connection, either by an Identity Provider (IdP) or LDAP Gateway. They are provisioned either via just-in-time or through synchronization and cannot be modified manually in PingOne.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internal Groups&lt;/strong&gt; are created manually within the PingOne portal like a Population and are a construct represented entirely in PingOne.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_roles.html#built-in-roles-tab&quot;&gt;&lt;strong&gt;Admin Role&lt;/strong&gt;&lt;/a&gt; is a collection of permissions that you can assign to Users and Groups. Admin Roles allow Users (and Users within the Groups) to interact with the PingOne platform at a higher level of privilege, such as managing the Users within a particular Population. &lt;strong&gt;Only Users and static Groups can be assigned Admin Roles.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;When to Create Multiple Environments&lt;/h2&gt;
&lt;p&gt;A popular use case of Environments is for building a promotion pipeline (e.g. development/staging/production/etc.), but additionally &lt;strong&gt;Environments are subdivided from one another in data and configuration&lt;/strong&gt; - meaning that while they share the same hosting resources they do not share the same PingOne Service Selection (e.g. one Environment can have PingOne MFA while another can have PingID for multi-factor), Policy Configuration (e.g. password policies, risk policies, mfa policies, DaVinci policies, etc.), Branding (e.g. theming, forms, localization), and user data (e.g. Populations/Groups/Users and their user information).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environments are great when you have requirements for Users, Applications, and Policies that strongly differ from one another.&lt;/strong&gt; Let’s look at an example of managing Employees vs Customers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Employee&lt;/th&gt;
&lt;th&gt;Customer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Factor Authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop and Mobile Authenticator&lt;/td&gt;
&lt;td&gt;Passkeys and Biometric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Risk Profiles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High Risk for New Device&lt;/td&gt;
&lt;td&gt;Low Risk for New Device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Localization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;English-Only (US Office)&lt;/td&gt;
&lt;td&gt;English, Spanish, and French Support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Onboarding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provisioning from an on-premise Directory&lt;/td&gt;
&lt;td&gt;Social Login with Google and Facebook with progressive profiling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The same can be said with business relationships. Perhaps you have a grouping of Suppliers who interact with your internal services - if so, it’s likely you’ll be securing and provisioning them in a way more similar to your internal (employee) population. On the other hand, if you are working with Distributors that are then interacting with end-customers, those individuals may be purchasing your products more similarly to an external (customer) population. These B2B2E and B2B2C relationships require drastically different interactions, and as such may benefit from separated Environments.&lt;/p&gt;
&lt;h2&gt;Populations Vs. Groups&lt;/h2&gt;
&lt;p&gt;At a first glance, Populations and Groups look kind of similar. However, they differ in some meaningful ways, namely:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Users must be part of a Population. Groups do not.&lt;/li&gt;
&lt;li&gt;Users and Groups can be a part of ONLY ONE Population. Users and Groups inside a Population can only be a part of Groups in that Population.&lt;/li&gt;
&lt;li&gt;Users and Groups can be a part of MORE THAN ONE Group.&lt;/li&gt;
&lt;li&gt;Groups can have Admin Roles assigned to them, Populations cannot.&lt;/li&gt;
&lt;li&gt;Admin Roles can be scoped to Populations, but not to Groups.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think of &lt;strong&gt;Populations as a representative set of Users&lt;/strong&gt; and &lt;strong&gt;Groups as a categorization of Users&lt;/strong&gt;. In the real world:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Population could be a Business Partner, while a Group inside that population could be one of their Departments.&lt;/li&gt;
&lt;li&gt;If you had a shared Application (let’s call it &lt;em&gt;Partner Hub&lt;/em&gt;) that all of your Business Partners use, a Group that spans multiple Populations could dynamically contain the Users that need access to that app.&lt;/li&gt;
&lt;li&gt;If you had Support personnel that aided your Business Partners, they could be in a group that has Admin Roles tied to the different Populations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Universal vs Segmented Identity&lt;/h2&gt;
&lt;p&gt;Depending on your business case your users may require a &lt;strong&gt;Universal Identity&lt;/strong&gt; or a &lt;strong&gt;Segmented Identity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;Universal Identity&lt;/strong&gt; is where &lt;strong&gt;one Identity has one User account for all Services&lt;/strong&gt;. This approach is advantageous in tightly-coupled partnerships such as a family of brands, a sports conference/league, a dealership, or a franchise. Universal Identities create seamless login experiences and greatly reduce your identity management footprint.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;Segmented Identity&lt;/strong&gt; is where &lt;strong&gt;one Identity has multiple User accounts for different Services&lt;/strong&gt;.This approach is advantageous in distinct relationships such as a supplier, branded enterprise application, an affiliate, or a distributor. Segmented Identities allow finer-grained control and administrative delegation into different levels of secure Applications and Users.&lt;/p&gt;
&lt;p&gt;Using Environments, Populations, and Groups, PingOne enables us to create both Universal and Segmented Identities. Consider the following table when setting up PingOne:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity Type / Service Type&lt;/th&gt;
&lt;th&gt;Separated Services&lt;/th&gt;
&lt;th&gt;Interconnected Services&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Segmented Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Different&lt;/strong&gt; Environments, Populations, and Groups&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Same&lt;/strong&gt; Environment and Group, &lt;strong&gt;Different&lt;/strong&gt; Populations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Universal Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Same&lt;/strong&gt; Environment and Population, &lt;strong&gt;Different&lt;/strong&gt; Groups&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Same&lt;/strong&gt; Environment, Population, and Group&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Given the top-level “unique-set” structure of Environments and Populations, PingOne is best suited for Segmented Identity at scale. Please review the &lt;a href=&quot;https://docs.pingidentity.com/pingone/getting_started_with_pingone/p1_platform_limits.html&quot;&gt;Standard Platform Limits&lt;/a&gt; to understand what approach fits your implementation and size.&lt;/p&gt;
&lt;h1&gt;Delegated Administration&lt;/h1&gt;
&lt;p&gt;Now that we have a grasp on the different segmentations of identity and configurations within PingOne, let’s set up our instance to take advantage of it.&lt;/p&gt;
&lt;p&gt;Our example in this guide will start with an out-of-the-box instance of PingOne. Feel free to work from an existing instance - our work here won’t override any of what you’ve done already.&lt;/p&gt;
&lt;p&gt;Your PingOne instance comes pre-loaded with a dedicated &lt;strong&gt;Administrators Environment&lt;/strong&gt;. This environment is where your “super-user” account lives and where you’ll add in most of your Administrators. Let’s leave this Environment alone for now while we set up some basic use-cases.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Your PingOne Instance and Administrators Environment&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Example Setup&lt;/h2&gt;
&lt;p&gt;To start, we are going to configure an example for each of the 4 different Service and Identity scenarios we defined in the prior section. To do so, let’s set up two Environments with some Populations, Groups, and Users in them.&lt;/p&gt;
&lt;p&gt;From your Environments View, select the plus “+” icon at the top of the page (or the “Create Environment” button in the “Manage Environments” modal if it’s still on your page).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating an Environment&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Creating The Customer Environment&lt;/h3&gt;
&lt;p&gt;First off, let’s create the Customer Environment. On the next page, select the “Customer solution” option. This will come pre-bundled with a variety of services that are common for CIAM deployments, such as Single-Sign-On, Multi-Factor Authentication (PingOne MFA), Risk Profiling (PingOne Protect), Identity Verification (PingOne Verify), and Orchestration (PingOne DaVinci).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt; &lt;img /&gt;
&lt;em&gt;The Environment Type and Services&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In Step 3, where you set the deployment options, input the following values and select “Finish”:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer Example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A testing Environment containing sample flows, policies, and users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate sample populations and users in this environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The closest region to you. I’m using North America (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The default license that comes with your instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Include a solution designer to easily design and test experiences&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Choose your Industry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Environment Deployment Options&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;While this Environment is deploying, let’s create the Workforce Environment.&lt;/p&gt;
&lt;h3&gt;Creating the Workforce Environment&lt;/h3&gt;
&lt;p&gt;Head back to the Environments view by clicking the Home icon or your Organization name in the top tab on your page just to the right of the Ping Identity logo. Mine is that &lt;code&gt;internal_davidgwizdala&lt;/code&gt; text you saw earlier.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Heading Home&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Back at the Environments view, create another new Environment like you did last time but rather than selecting “Customer solution” we’ll be selecting “Workforce solution” instead. You’ll notice that the services deployed will be slightly different, most notably that PingOne MFA has been replaced by PingID and PingOne Verify has been removed.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;img /&gt;
&lt;em&gt;The Workforce Environment Type and Services&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In Step 3, where you set the deployment options, input the following values and select “Finish”:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workforce Example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A testing Workforce Environment containing sample flows, policies, and users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate sample populations and users in this environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The closest region to you. I’m using North America (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The default license that comes with your instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Include a solution designer to easily design and test experiences&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Choose your Industry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Workforce Environment Deployment Options&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now that we’ve created some example Environments to play with, let’s see what samples we are starting with.&lt;/p&gt;
&lt;h3&gt;Reviewing the Setup&lt;/h3&gt;
&lt;p&gt;After setup is complete for both your Customer and Workforce Environments, you’ll see that the steps you’ve taken have set up a few things for you. Click into each of the Environments to check for the following samples:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Under Directory → Users, a series of Users have been added to your Environment.&lt;/li&gt;
&lt;li&gt;Under Directory → Groups, two Groups have been added: &lt;strong&gt;Sample Group&lt;/strong&gt; and &lt;strong&gt;Another Sample Group&lt;/strong&gt;. If you click into the Group Details (by clicking the Group) you’ll see that Sample Group is not tied to any Population while Another Sample Group has been tied to the More Sample Users Population.&lt;/li&gt;
&lt;li&gt;Under Directory → Populations, you’ll have been given two Populations: &lt;strong&gt;Sample Users&lt;/strong&gt; and &lt;strong&gt;More Sample Users&lt;/strong&gt;. These Populations come pre-loaded with their Users and, in the case of the More Sample Users Population, their own Group.&lt;/li&gt;
&lt;li&gt;Under Applications → Applications, you’ll see that you have some Applications already created:
&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Getting Started Application&lt;/strong&gt;, which is the example application you saw created in the “Getting Started” tab of your Environment and is what is attached to the Solutions Designer you enabled earlier.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;PingOne Admin Console&lt;/strong&gt;, which is where your administrators &lt;strong&gt;created in this Environment&lt;/strong&gt; will go to manage configuration and identity data for this Environment.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;PingOne Application Portal&lt;/strong&gt;, which is where your Users can go to access their applications (think of this as an Application Dock)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;PingOne DaVinci Connection&lt;/strong&gt;, which links the Environment to orchestration in PingOne&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;PingOne Self-Service - MyAccount&lt;/strong&gt;, which is where your Users can go to manage their own profile details, passwords, and MFA devices.&lt;/li&gt;
&lt;li&gt;Under the Workforce Environment, you’ll also have the &lt;strong&gt;PingID Desktop&lt;/strong&gt; and &lt;strong&gt;PingID Mobile&lt;/strong&gt; applications, which allow your workforce to use the PingID authenticator for Multi-factor authentication. We’ll leave these be for the sake of this example.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With this setup, we have a working demonstration of both how Customers and Employees can interact with PingOne. Now let’s delegate some permissions to it.&lt;/p&gt;
&lt;p&gt;Since this guide is focused on Delegated Administration, we’re going to specifically use the &lt;strong&gt;PingOne Admin Console&lt;/strong&gt; Application. Copy and save the Admin Console URL from the &lt;strong&gt;Administrators Environment&lt;/strong&gt;, either from the Applications page, the Environment Properties (Settings → Environment Properties), or Environment Summary tab (on the Environments List page).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;img /&gt;&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Finding the Admin Console URL&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Creating the Delegated Administrators&lt;/h3&gt;
&lt;p&gt;Remember the Administrators Environment we mentioned earlier? This Environment is the hub where we’ll be assigning permissions for Users to Environments, Populations, and Applications.&lt;/p&gt;
&lt;p&gt;Select the Administrators Environment from your Environments list, copying the Self Service URL on the details tab before entering, and then click on Directory → Users.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Saving the Admin Environment Self-Service URL for Later&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We are going to be creating a couple different users to administer the Environments we created. Click the Plus “+” icon next to the Users header, enter in each of the following User details, and hit “Save” when done. You’ll be creating two users in total.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;User 1&lt;/th&gt;
&lt;th&gt;User 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Given Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer&lt;/td&gt;
&lt;td&gt;Workforce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Family Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Username&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;customerAdmin&lt;/td&gt;
&lt;td&gt;workforceAdmin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An email you can access&lt;/td&gt;
&lt;td&gt;An email you can access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Require Email to be Verified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;td&gt;True (checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Population&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Administrators Population&lt;/td&gt;
&lt;td&gt;Administrators Population&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authoritative Identity Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PingOne (default)&lt;/td&gt;
&lt;td&gt;PingOne (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ch4ngeIt!&lt;/td&gt;
&lt;td&gt;Ch4ngeIt!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Email Addresses for all admin accounts (i.e. accounts with delegated roles) &lt;strong&gt;must be verified&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Next, let’s verify these accounts so that we can perform delegated administration. In an incognito, guest, or separate browser window, go to the Self-Service URL for the Admin Environment that you copied earlier. Enter in the username and password of your administrators - you’ll then be presented with a change password prompt.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Change Password Prompt&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enter in the current password and then a new password of your choice. You’ll then be requested to enter in the verification code that was sent to your User when you created their account.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Entering in the Verification Code&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Repeat the above steps for the Workforce Administrator. When you return to your Administrator Environment and check the Users, you’ll see that the status of their email address is “Verified”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Verified Admin Email Address&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Delegating Administration by User&lt;/h2&gt;
&lt;p&gt;Right now, these Administrators are a part of the Administrators Environment but don’t have any specific permissions. Remember the Console URL we copied earlier?  Paste it into its own separate browser (or guest account) window and try to log in as either of the admins you just created. You’ll encounter an “Incorrect username or password. Please try again.” message.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Trying to Log In without Permissions&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, let’s give these users some permissions. Starting with the Customer Admin, select the “Roles” tab in their user profile and then “Grant Roles”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Granting Admin Roles to a User&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From here, you’ll see a wide variety of permissions available to assign. Let’s start at the highest level and work our way down.&lt;/p&gt;
&lt;p&gt;First, set your Customer Admin to be an Environment Admin of the Customer Example Environment and hit Save. This gives your admin permission to View, Create, Update, and Delete configuration within the Environment. More details can be found in the info “i” icon next to the title, but for all intents and purposes &lt;strong&gt;think of the Environment Admin the manager of Environment Configuration.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding Environment Permissions&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you try logging in again, you’ll see that you’re now prompted with MFA enrollment (a default setting for Admin Users). Go ahead and enroll in a device of your choice.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Enrolling MFA as an Admin&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You’ll see that your Customer Administrator has access to configuration details such as Monitoring, Applications, Policies, Roles, and Experience but doesn’t have the capability to see or manage Users directly.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Environment Admin’s Capabilities&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most Admin Roles give us the capability to assign by Organization or Environment (see DaVinci Admin, Application Admin), but let’s get a bit more granular. Back in the Administrators Environment view, update the Customer Admin so that they &lt;strong&gt;don’t&lt;/strong&gt; have the Environment Administrator Admin Role and &lt;strong&gt;do&lt;/strong&gt; have the Identity Data Admin Role specifically for the “Sample Users” Population. To do that, select the Identity Data Admin Role, 1) click the Filter icon (next to the checkbox) and 2) select the Sample Users Population. When you’re done, you should see “Limited Access” next to the Population and then the specific population after saving in the User summary.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt; &lt;img /&gt; &lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Limiting Access to the Sample Users Population&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now go back to your customerAdmin’s dashboard. After refreshing the page, you’ll see that most of your access has been revoked to Read-Only but now you can manage the Users within the Sample Population.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;
&lt;em&gt;Identity Data Admin View&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Using what you just learned, assign the same Admin Role to your Workforce Admin but with their permission pointed to the Sample Users Population in the Workforce Environment.&lt;/p&gt;
&lt;h2&gt;Delegating Administration by Group&lt;/h2&gt;
&lt;p&gt;So right now we have a User that can manage the Identity Data of one Population in one Environment. Likely, as your organization expands, it’ll become unwieldy (and error-prone) to assign these permissions one at a time. Rather than assigning on a User level, let’s assign to a Group instead.&lt;/p&gt;
&lt;p&gt;Inside the Administrators Environment, under Directory → Groups, select the Plus “+” icon and give it the Group Name “Helpdesk”. This group is going to be able to manage all of the identities for both the customer and workforce environments.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating the Group&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This Group is assigned outside of a Population so we can add any Users that are in this Environment to it. With that in mind, go to the Users tab in the Helpdesk group and select “Add Individually”. We mentioned this before, but to protect from accidental overpermissioning &lt;strong&gt;Administrative Groups Permissions CANNOT be assigned via a filter.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Adding Individual Users to the Helpdesk Group&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On the next screen, select the customerAdmin and the workforceAdmin Users to this Group.&lt;/p&gt;
&lt;p&gt;Next, go to the Roles Tab and select “Grant Roles”. This screen will look very familiar to you: it’s the same screen we used when assigning Admin Roles to Users!&lt;/p&gt;
&lt;p&gt;This time, let’s give this Group the Identity Data Admin Role across both the Customer Example and Workforce Example Environments.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;Assigning the Identity Data Admin Roles&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When you hit Save, and go back to your customerAdmin or workforceAdmin User, you’ll see that their permissions have been “Granted By Group” to the two Environments.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The User’s Assigned Admin Roles, Direct and Group&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You probably also saw an error message pop up - we’ve just tried to assign the same Admin Role twice to our Admin Users. The highest amount of permission will win here: in this case, the Helpdesk Identity Data Admin Role that grants access to all Users in both Environments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go ahead and refresh the admin portal for either your customerAdmin or your workforceAdmin. Not only will you see all 40 users in your current Environment, you’ll additionally be able to navigate into and see the other Environment too.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;img /&gt;&lt;br /&gt;
&lt;em&gt;The Assigned Group Permissions to the End User&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Putting it to Practice&lt;/h2&gt;
&lt;p&gt;So now you know how to assign Admin Roles to a User and to a Group - let’s try some common combinations that will help you and your Users best interact with the platform. If you want a challenge, try to assign the right roles before reading which options you need.&lt;/p&gt;

  Managing Data about Users
&lt;ul&gt;
&lt;li&gt;Identity Data Admin&lt;/li&gt;
&lt;/ul&gt;


  Creating New Applications and Managing Specific Applications
&lt;ul&gt;
&lt;li&gt;Client App Developer&lt;/li&gt;
&lt;li&gt;Application Admin&lt;/li&gt;
&lt;/ul&gt;


  Defining Authentication Policies and Experiences
&lt;ul&gt;
&lt;li&gt;Environment Admin&lt;/li&gt;
&lt;li&gt;DaVinci Admin&lt;/li&gt;
&lt;/ul&gt;


  User Experience Tester
&lt;ul&gt;
&lt;li&gt;DaVinci Admin&lt;/li&gt;
&lt;li&gt;Identity Data Admin (you need a test user to test a flow!)&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;Extending Delegated Administration&lt;/h1&gt;
&lt;p&gt;We’ve barely scratched the surface for what Delegated Administration can do within PingOne. To give you some jumping off points, take a look at common ways the Delegated Administration model is extended within the platform.&lt;/p&gt;
&lt;h2&gt;In-Environment Admins (Partner Admins)&lt;/h2&gt;
&lt;p&gt;Remember when we first looked at the Administrators Environment and said that &lt;strong&gt;most&lt;/strong&gt; delegated administrators would be in this environment? Let’s talk through the scenarios in which you may want to &lt;strong&gt;assign delegated administration in a non-admin Environment&lt;/strong&gt; instead. Think of this section as the &lt;strong&gt;Universal Identity&lt;/strong&gt; vs. Segmented Identity (which we did in the last section) scenario.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The delegated administrator utilizes the same policies (e.g. password, MFA, DaVinci, Risk) as the environment or identities that they manage (e.g. service user)&lt;/li&gt;
&lt;li&gt;The delegated administrator requires a distinct login experience (e.g. UI/Branding/Flow) indicative of the partnership or brand they are coming from (e.g. branding director)&lt;/li&gt;
&lt;li&gt;The delegated administrator accesses the same Applications that their users are managing, perhaps with a different authorization policy (e.g. Application manager)&lt;/li&gt;
&lt;li&gt;The delegated administrator Federates from the same IdP as their users (e.g. external helpdesk, business owner)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In these cases (and more!) ensure that your delegated administrators are Grouped in meaningful ways - the more that the Admin Roles are defined at a Group level the easier it will be to track what permissions each of your Users have.&lt;/p&gt;
&lt;h2&gt;Custom Admin Roles&lt;/h2&gt;
&lt;p&gt;While PingOne provides a series of Admin Roles out of the box for you and your administrators to utilize, you can create more catered Admin Role permissions through the use of &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_custom_role_add.html&quot;&gt;Custom Admin Roles&lt;/a&gt;. These allow you to set CRUD-based permissions on the same set of Administrative Roles we were working with in the other section.&lt;/p&gt;
&lt;p&gt;Since the Documentation contains &lt;a href=&quot;https://docs.pingidentity.com/pingone/directory/p1_custom_roles_scenarios_intro.html&quot;&gt;Custom Admin Role Scenarios&lt;/a&gt; for you to look at, I’m not going to provide a walkthrough here. That being said, combining Custom Admin Roles with your Groups and Users gives you the ultimate level of control across your Environments.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Through this Guide we have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learned about the key definitions and differences between structures within PingOne&lt;/li&gt;
&lt;li&gt;Created our own unique example Environments containing these structures&lt;/li&gt;
&lt;li&gt;Delegated administration to external users to manage the different structures.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Additionally, we have looked at ways that Ping extends this capability through the user of assigning Admin Roles within non-Admin Environments (“Partner Admins”) and through the creation of Custom Admin Roles.&lt;/p&gt;
&lt;p&gt;With what you have learned today you should be able to confidently create and assign complex Admin Role relationships for your internal and external administration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.1PUsVrVE_Z16vVkj.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Using the new Use Case Library for fast onboarding with PingOne APIs</title><link>https://developer.pingidentity.com/blog/use-case-library-fast-onboarding/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/use-case-library-fast-onboarding/</guid><description>Our redesigned Getting Started and Use Case Library Postman collections help you fast-track your PingOne API journey.</description><pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;PingOne APIs pull back the curtain to reveal precisely how PingOne sign-on flows work. From application configuration and user registration to calling the PingOne authorization server to initiate a flow, the APIs show you the detailed request and response data for each step. Whether you want a simple username/password flow, a passwordless flow, or something more intricate, the PingOne APIs are a powerful tool for getting the job done and for understanding how PingOne works.&lt;/p&gt;
&lt;h2&gt;Syncing the Postman collections&lt;/h2&gt;
&lt;p&gt;The PingOne API developer doc library contains two key resources to fast-track your API journey: Getting Started and Use Case Library. The three tasks in Getting Started walk you through the process of getting a PingOne admin access token and using that token to call PingOne APIs.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;First, use the admin console to create the PingOne admin Worker application needed to issue an admin access token that gives you permission to run PingOne APIs. With an admin access token, you can then create a test environment, configure a web application, and define a test user, which are essential resources for running sign-on flows.&lt;/p&gt;
&lt;h2&gt;Exploring the use cases&lt;/h2&gt;
&lt;p&gt;The use cases align with PingOne best practices and give you steps to configure and run a wide range of sign-on flows. Building on the assets you created in Getting Started, the use cases streamline the environment and runtime endpoint configurations. For example, PingOne supports five methods for authenticating the token request. To explore how the flow differs based on token authentication method, the Use Case Library Postman collection contains a simple PUT request that operates on the asset you created in Getting Started to set the app configuration for your chosen method.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;In addition, the Postman collection includes runtime endpoints precisely configured to test the flow based on your application settings. You can see the parameters required by the authorization request to initiate the flow based on your application’s configuration, and the properties needed in the token request to return an access token.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;The Use Case Library contains a diverse collection of flows, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Device authorization for OAuth-enabled devices, such as smart TVs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Consent agreement to obtain a user’s agreement to a policy.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Progressive profiling to add data to a user’s profile.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Multifactor authentication flows for greater security.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Passwordless authentication flows.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Proof Key for Code Exchange (PKCE) authentication flows.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DaVinci sign-on and registration flows.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These use cases show you how the configuration and runtime endpoints work together to build the flow. Think of them not only as a hands-on tutorial but also as a starting point for further development and experimentation with PingOne APIs.&lt;/p&gt;
&lt;h2&gt;Let us know what you think&lt;/h2&gt;
&lt;p&gt;Download the Postman collections for your favorite use cases and give it a try. If you have ideas about use cases that we haven’t covered in the updated content, let us know by clicking the feedback icon in the top-right corner of any page.&lt;/p&gt;
&lt;p&gt;Enjoy the ride!&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Try &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/getting-started/&quot;&gt;Getting Started&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Try &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/workflow-library/&quot;&gt;Use Case Library&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check out the &lt;a href=&quot;https://developer.pingidentity.com/&quot;&gt;Developer Portal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check out the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity Developer Community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.BOqoeYJk_ZRA2Fe.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>App-to-web session transfer: the security perspective</title><link>https://developer.pingidentity.com/blog/app-to-web-session-transfer-the-security-perspective/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/app-to-web-session-transfer-the-security-perspective/</guid><description>Exploring the security implications and enhancements of app-to-web session transfer patterns — examining OAuth risks, device binding strategies, and layered security controls for mobile-to-web SSO.</description><pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@robertotto_31404/app-to-web-session-transfer-the-security-perspective-4c9ede049002&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This post is the third in a series; in the first, we discuss the &lt;a href=&quot;https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/&quot;&gt;app-to-web session transfer problem&lt;/a&gt; and propose a secure approach using OAuth 2.0 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9126&quot;&gt;Pushed Authorisation Requests&lt;/a&gt; (PAR). In the second, we walked through a &lt;a href=&quot;https://developer.pingidentity.com/blog/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform/&quot;&gt;detailed implementation blueprint&lt;/a&gt; using the PingOne platform. This third part will dive specifically into the security implications of this approach and a number of potential enhancements we could consider. &lt;em&gt;Familiarity with the previous two parts is recommended before diving into this one.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Understanding the risks&lt;/h2&gt;
&lt;p&gt;First, a disclaimer. What follows is not exhaustive — nor could it ever be. I urge anyone reading and following this advice to remember that what I’m providing here is certainly not meant to be iron-clad security guidance, however well-reasoned I like to think it may be!&lt;/p&gt;
&lt;p&gt;Nothing that we implement in any scenario should ever be considered 100% secure anyway (not in a world where we have integrated systems with boundaries — not to mention real-life users!) Our challenge is always to do what we can to predict the threats we might face, assess the likelihood and impact of each such risk and then critically assess our proposed solution against both aspects.&lt;/p&gt;
&lt;p&gt;If we consider the pattern end-to-end, we should be able to identify a number of potential weak points where a motivated and skilled adversary could compromise security. The first is not a new or additional risk factor, given that we are using OAuth 2 to secure a native mobile application since it relates to the usage of an access token as a &lt;strong&gt;bearer token&lt;/strong&gt; to allow API access. This implies that the utmost care must be taken within the end-to-end system architecture (but particularly within the client app itself) to protect this access token from unauthorised access or exfiltration.&lt;/p&gt;
&lt;p&gt;We can argue that our PAR-based approach does not create any additional risk of access token exposure beyond what is already present anyway. When our application makes an authenticated call to a backend API, it initiates an HTTPS request and includes the access token in an authorisation header. The PAR approach includes the access token as a POST body parameter over HTTPS to our authorisation server and since we trust the server and the orchestration logic it executes, this should not materially affect our exposure risk.&lt;/p&gt;
&lt;h2&gt;Additional controls&lt;/h2&gt;
&lt;p&gt;We have added a number of additional controls to guard against specific risks, including the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;We use a dedicated session transfer client to initiate the PAR process and intentionally restrict this client’s capabilities with the &lt;em&gt;“web-session-only”&lt;/em&gt; scope. By configuring this client with a meaningless scope, an invalid redirect_URI and by including a PKCE Code Challenge we add a number of layered obstacles to prevent misuse of this client — that is in addition to specific orchestration logic to prevent the successful issue of any access token to this client.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The PAR response contains a &lt;em&gt;request_uri&lt;/em&gt; parameter and this, while inherently single-use and short-lived, does create a potential security risk since it could be exfiltrated and copied to a different device in order to hijack the web session that it creates. We mitigate this risk by further constraining the useful life of the request (via our 10-second &lt;em&gt;exp&lt;/em&gt; claim window) and by performing an IP check ahead of creating the web session.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The missing piece: device binding&lt;/h2&gt;
&lt;p&gt;One significant risk that we have not adequately addressed is that of access token exfiltration followed by a spoofed PAR request from a different client. Given that the access token itself is all that conveys the authenticated user context, we need to ensure that should an attacker obtain a valid access token by some nefarious means, they would not be able to use this token to create a valid web session on a different device.&lt;/p&gt;
&lt;p&gt;Our remaining challenge is thus to implement some mechanism to ensure that PAR request is initiated (and completed) on the same device that obtained the access token initially.&lt;/p&gt;
&lt;p&gt;We should, at this point, look critically at the decision to use IP address as the mechanism for binding the backend and front-end requests. While it is arguably the simplest thing to implement, IP address is typically a rather weak proxy for real device identity since it can be almost trivially replicated via proxy.&lt;/p&gt;
&lt;p&gt;It is tempting to consider binding the access token to the device IP address at the time of initial login and this approach certainly would be more secure than simply echoing the current IP at the time of the PAR call. As a counter, though, the nature of mobile devices means that we should expect that a device’s IP address could change during the course of a session as the user moves from one network to another. We could end up restricting our application to a single IP address for the duration of the session if we were to follow this approach.&lt;/p&gt;
&lt;h2&gt;Extending the solution with PingOne MFA&lt;/h2&gt;
&lt;p&gt;Within our toolkit, we have the &lt;a href=&quot;https://docs.pingidentity.com/pingone/strong_authentication_mfa/p1_strong_authentication_start.html&quot;&gt;PingOne MFA&lt;/a&gt; service, Ping Identity’s strong authentication solution and one simple way for us to further improve our security posture here would be to use the &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/native-sdks/pingone-mfa-mobile-sdks/pingone-mfa-mobile-sdk-flows.html&quot;&gt;Automatic Device Authorisation&lt;/a&gt; capability offered by the &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/native-sdks/pingone-mfa-mobile-sdks.html&quot;&gt;PingOne MFA Mobile SDK&lt;/a&gt;. This approach allows us to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;identify a device through a cryptographic payload&lt;/li&gt;
&lt;li&gt;bind that device to a user record&lt;/li&gt;
&lt;li&gt;perform ad-hoc verification of that device based on a provided payload&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s revisit the end-to-end process description from two posts back — looking at how PingOne MFA is used to extend and further secure the flow.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When Alice first registers her account with the Acme Inc native application, the app code embeds the PingOne MFA SDK, which generates a cryptographic device payload (bound to a private key in Alice’s key-store). This payload is passed to the PingOne backend as part of Alice’s registration request where the associated public key is paired with Alice’s user profile. The device is thus bound to Alice’s account.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alice opens the Acme Inc native application and clicks the “Login” button. The app uses the PingOne MFA SDK to generate a signed payload and includes this payload with the authentication request sent to the PingOne backend.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;She is authenticated via a native authentication experience (no browser pop-up) that uses DaVinci flow logic to collect Alice’s user credentials, validate these against the user directory and then calls the PingOne MFA backend to validate the payload from the device and ensure that the extracted device ID is bound to Alice’s account.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The flow completes successfully and includes the device ID as a claim in the access token that is sent to the app. The app responsibly and securely stores this access token (which is bound to her device via the &lt;em&gt;device_ID&lt;/em&gt; claim) on Alice’s behalf.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alice clicks the “Manage my profile” button in the app. The app code starts the PAR process using the client_id of the dedicated Session Transfer Client. It includes a scope such as “session_transfer” in the request, and generates a PKCE Code Challenge that it includes with the request. Note that the corresponding Code Verifier can be discarded at this point since this flow is not intended to ever result in an access token issuance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The app code makes a call to the PingOne MFA SDK to generate a fresh device payload. It then includes this payload, along with the access token that it already holds for the user, and the URL of the profile management application, in the PAR body that it constructs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It sends the PAR request to the authorisation server and obtains a response that contains a request_uri, which it appends to the AS’s authorization endpoint URL before opening a browser tab to initiate the front-channel flow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When the front-channel request hits the Authorisation Server, the request is validated (to ensure it has not expired or already been used) before being handed to the identity orchestration engine for further processing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The DaVinci orchestration engine must introspect or validate the provided access token to ensure that it is valid, has not expired and was indeed issued to the expected client (the mobile app). It should also extract the &lt;em&gt;device_ID&lt;/em&gt; claim from the token. It then calls the PingOne MFA backend to validate the payload from the device and ensure that the extracted device ID is bound to Alice’s account. It also ensures that the device ID extracted from the device payload matches the &lt;em&gt;device_ID&lt;/em&gt; claim extracted from the access token.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Should the above checks succeed, the orchestration engine extracts the “sub” claim from the access token to determine the appropriate user, and sets a session cookie in the browser for that same user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At this point, the orchestration logic simply redirects the browser to the URL of the profile management application (the same URL that was provided via the PAR back-channel initiation in step 6). This is the step that interrupts the flow started by the Session Transfer Client and ensure that no auth code or access token is ever issued.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The profile management application now receives the browser redirect and is responsible for initiating its own OAuth/OpenID Connect Auth Code flow, just as it would had a browser opened the app URL directly. It should generate its own redirect to the AS, including its own client_id and redirect_uri, its own appropriate scopes and its own PKCE Code Challenge. The AS will start a user authentication process that should complete with no further user interaction required, based on the presence of the session cookie set in step 10.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The browser is thus redirected back to the profile management application with everything that is needed to display logged-in user content.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The above approach certainly closes a number of potential security loopholes; however the lift is admittedly rather heavy as a result. I would advise a comprehensive review of your own risk landscape (in consultation with your security team) in order to decide whether the additional overhead is warranted in your specific scenario.&lt;/p&gt;
&lt;h2&gt;In conclusion&lt;/h2&gt;
&lt;p&gt;This has been quite the journey — from a frustrated user staring at a “Contextual Cliff” to a high-assurance architecture that even a cynical CISO could love.&lt;/p&gt;
&lt;p&gt;To wrap up this series, let’s look at the path we’ve carved out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/&quot;&gt;&lt;strong&gt;Part 1: The Blueprint.&lt;/strong&gt;&lt;/a&gt; We defined the “Session Transfer Problem” and why relying on the “serendipitous” creation of browser cookies is no longer a viable strategy in a privacy-first, sandboxed world.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.pingidentity.com/blog/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform/&quot;&gt;&lt;strong&gt;Part 2: The Implementation.&lt;/strong&gt;&lt;/a&gt; We got our hands dirty with Kotlin and DaVinci, moving sensitive session data off the URL and into the secure back-channel via &lt;strong&gt;OAuth 2.0 Pushed Authorization Requests (PAR)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: The Hardening.&lt;/strong&gt; We moved from “making it work” to “making it resilient,” exploring the nuances of IP binding and the “gold standard” of cryptographic &lt;strong&gt;MFA Device Binding&lt;/strong&gt; to ensure the session stays where it belongs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reality of modern digital products is that hybrid architectures aren’t going anywhere. Our job as identity professionals isn’t to fight the existence of web-views, but to ensure that the transition between native and web remains invisible to the user and invincible to the attacker.&lt;/p&gt;
&lt;p&gt;Identity orchestration gives us the tools to build these “bridges” without compromising on either side of the UX-Security trade-off.&lt;/p&gt;
&lt;h2&gt;Over to You&lt;/h2&gt;
&lt;p&gt;I’d love to hear from the community:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How are you currently tackling session bridging in your own environments?&lt;/li&gt;
&lt;li&gt;Is the “heavy lift” of MFA-backed device binding a non-starter for your product teams, or has it become a non-negotiable requirement for your security auditors?&lt;/li&gt;
&lt;li&gt;Are there other “Contextual Cliffs” in your applications that you’re struggling to smooth over?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drop a comment in the developer community with your thoughts, questions, or your own battle stories from the IAM trenches!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CTi6Rdvk_Z15JE7d.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Implementing secure session transfer: a field guide using the PingOne platform</title><link>https://developer.pingidentity.com/blog/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform/</guid><description>A hands-on field guide to wiring up a secure app-to-web session transfer on Android — walking through PingOne DaVinci flows, Pushed Authorization Requests, and PKCE to deliver seamless, token-validated SSO from a native mobile app to a web browser.</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@robertotto_31404/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform-a0410709e073&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my &lt;a href=&quot;https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/&quot;&gt;previous article&lt;/a&gt;, I established the conceptual blueprint for a hardened, standards-based mobile session transfer pattern. We identified that relying on shared browser cookies is a fragile strategy, often breaking under the weight of modern tracking prevention like Apple’s ITP or Google’s Privacy Sandbox. The alternative is a robust “bridge” architecture, moving beyond basic OAuth flows to integrate &lt;strong&gt;Pushed Authorization Requests (PAR, RFC 9126)&lt;/strong&gt; with Identity Orchestration for a secure session transition.&lt;/p&gt;
&lt;p&gt;In this field guide, I am moving beyond the “why” and deep into the “how, to provide a technical, domain-by-domain breakdown of the actual implementation.&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;p&gt;This article is designed as a direct sequel, moving straight into the implementation mechanics. As such, I am assuming you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A fundamental understanding of the PAR and IP binding concepts established in &lt;a href=&quot;https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/&quot;&gt;part 1&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Access to a PingOne environment with SSO and DaVinci enabled.&lt;/li&gt;
&lt;li&gt;A basic proficiency with Kotlin/JetPack Compose (Android) although what is described here could be implemented in any other mobile platform with little difficulty&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Initial PingOne configuration&lt;/h2&gt;
&lt;p&gt;For this setup, we need the following to be configured in PingOne:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A Native Application client (this represents our mobile app and allows that app to obtain an access token using the SDK)&lt;/li&gt;
&lt;li&gt;A Session Transfer Application client (a stripped down client that exists only to trigger the PAR process)&lt;/li&gt;
&lt;li&gt;Some other “OIDC Web App” (I will be using the preconfigured “PingOne Self Service — MyAccount” application that is automatically bootstrapped with any environment)&lt;/li&gt;
&lt;li&gt;A user account for testing&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Native Application client (1) should be configured as follows:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;Note here that &lt;em&gt;“API”&lt;/em&gt; is a custom &lt;a href=&quot;https://docs.pingidentity.com/pingone/applications/p1_resources.html&quot;&gt;Resource&lt;/a&gt; defined in PingOne with a corresponding &lt;em&gt;“application_api”&lt;/em&gt; scope. This represents access to native application backend API’s that will be granted via the application access token.&lt;/p&gt;
&lt;p&gt;We configure a “Simple U+PW” DaVinci flow to enable native user authentication using the &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/davinci/index.html&quot;&gt;Ping SDK&lt;/a&gt;. This flow will be described later.&lt;/p&gt;
&lt;p&gt;The Session Transfer Application client (2) should be configured as follows:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;This client is also a public client (hence &lt;em&gt;Token Endpoint Authentication Method = None&lt;/em&gt;) but note that here we specify an invalid/unused &lt;em&gt;Redirect URI&lt;/em&gt; since this client will only be used for PAR initiation, not token issuance. We require a Pushed Authorization Request with a short timeout and also define a custom scope of &lt;em&gt;web_session_only&lt;/em&gt;. The combination of the invalid Redirect URI, the custom scope and the PKCE enforcement should provide sufficient (and redundant) protection against this client being misused for token issuance.&lt;/p&gt;
&lt;p&gt;The “Session Transfer” DaVinci Flow will be described later.&lt;/p&gt;
&lt;h2&gt;The first DaVinci flow&lt;/h2&gt;
&lt;p&gt;In order to deliver a fully-native login experience within our application, we will implement the &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/davinci/index.html&quot;&gt;PingSDK with the DaVinci Client module&lt;/a&gt;. This approach requires us to define a PingOne DaVinci authentication flow for initial authentication of the user within our application (the step that allows us to obtain an access token).&lt;/p&gt;
&lt;p&gt;For this example we will use a trivially simple DaVinci flow, shown below. We use the bootstrapped “Example — Sign On” form from the &lt;a href=&quot;https://docs.pingidentity.com/pingone/user_experience/p1_forms.html&quot;&gt;PingOne Forms&lt;/a&gt; service.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;A more detailed explanation of how to use DaVinci to enable authentication is well beyond the scope of this discussion, but should the above prove insufficient, help is always available via the comments section!&lt;/p&gt;
&lt;h2&gt;Using the Ping SDK to get a user access token&lt;/h2&gt;
&lt;p&gt;Paul Simon told us that there are 50 ways to leave your lover — what I will add to this insight is that there are at least twice as many ways to obtain an access token within a native application! I’m going to provide some very simple sample code below showing just one of these, using Android with Kotlin and JetPack Compose. Here, we use the &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/davinci/index.html&quot;&gt;PingSDK&lt;/a&gt; to trigger the above DaVinci flow to perform simple username and password authentication.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; MainActivity&lt;/span&gt;&lt;span&gt; : ComponentActivity() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sealed&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt; DaVinciState&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        object Idle : &lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        data class &lt;/span&gt;&lt;span&gt;Waiting&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; text&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        data class &lt;/span&gt;&lt;span&gt;ShowForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; collectors&lt;/span&gt;&lt;span&gt;: List&amp;lt;Collector&amp;lt;*&amp;gt;&amp;gt;) : &lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        data class &lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; message&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;) : &lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        data class &lt;/span&gt;&lt;span&gt;Success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; user&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;?) : &lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var currentState by mutableStateOf&amp;lt;&lt;/span&gt;&lt;span&gt;DaVinciState&lt;/span&gt;&lt;span&gt;&amp;gt;(DaVinciState.Idle)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var userID: String? = null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var username: String? = null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var accessToken: String? = null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    val CLIENT_ID = &quot;&amp;lt;&lt;/span&gt;&lt;span&gt;PingOne&lt;/span&gt;&lt;span&gt; Client&lt;/span&gt;&lt;span&gt; ID&lt;/span&gt;&lt;span&gt; for&lt;/span&gt;&lt;span&gt; Native&lt;/span&gt;&lt;span&gt; Application&lt;/span&gt;&lt;span&gt; Client&lt;/span&gt;&lt;span&gt;&amp;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    val P1_BASE_URL = &quot;https:&lt;/span&gt;&lt;span&gt;//auth.pingone.eu/&amp;lt;PingOne Environment ID&amp;gt;/&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    val daVinci = DaVinci {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        module&lt;/span&gt;&lt;span&gt;(Oidc) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            clientId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; CLIENT_ID&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            discoveryEndpoint &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; P1_BASE_URL &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot;as/.well-known/openid-configuration&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            scopes &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; mutableSetOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;openid&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;application_api&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            redirectUri &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;com.example.myapp://oauth2redirect&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; node&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Node&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    override fun &lt;/span&gt;&lt;span&gt;onCreate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;savedInstanceState&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Bundle&lt;/span&gt;&lt;span&gt;?) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        super&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;onCreate&lt;/span&gt;&lt;span&gt;(savedInstanceState)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        enableEdgeToEdge&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        setContent {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            SessionTransferTheme {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                MainLayout&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    suspend fun &lt;/span&gt;&lt;span&gt;startNativeLogin&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        node &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; daVinci.&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handleNode&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    suspend fun &lt;/span&gt;&lt;span&gt;submitCreds&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val submitCollector &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ((node as ContinueNode).collectors[&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;]) as SubmitCollector&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        submitCollector.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; submitCollector.key&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.&lt;/span&gt;&lt;span&gt;Waiting&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        node &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (node as ContinueNode).&lt;/span&gt;&lt;span&gt;next&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handleNode&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    suspend fun &lt;/span&gt;&lt;span&gt;handleNode&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        when&lt;/span&gt;&lt;span&gt; (node) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            is ContinueNode &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.&lt;/span&gt;&lt;span&gt;ShowForm&lt;/span&gt;&lt;span&gt;((node as ContinueNode).collectors)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            is ErrorNode &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.&lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Login Error&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            is FailureNode &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.&lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Login Failure&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            is SuccessNode &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                val user &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; daVinci.&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                val userInfo&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;JsonObject, OidcError&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    user&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;userinfo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if&lt;/span&gt;&lt;span&gt; (userInfo is Result.Success) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    userID &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; userInfo.value[&lt;/span&gt;&lt;span&gt;&quot;sub&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.jsonPrimitive&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.content&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    username &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; userInfo.value[&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.jsonPrimitive&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.content&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                val tokenResponse &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; user&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                if&lt;/span&gt;&lt;span&gt; (tokenResponse is Result.Success) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; tokenResponse.value&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    accessToken &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; token.accessToken&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.&lt;/span&gt;&lt;span&gt;Success&lt;/span&gt;&lt;span&gt;(user)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            else&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    @&lt;/span&gt;&lt;span&gt;Composable&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fun &lt;/span&gt;&lt;span&gt;MainLayout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;modifier&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Modifier&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;Modifier&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val scope &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; rememberCoroutineScope&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        Column&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; modifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;fillMaxSize&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;(24.dp),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            horizontalAlignment &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Alignment.CenterHorizontally,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            verticalArrangement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Arrangement.Center&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        ) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            when&lt;/span&gt;&lt;span&gt; (val state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; currentState) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.Idle &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    // LOGGED OUT STATE&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Secure Session Transfer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.headlineMedium&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(8.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Authenticate to begin the app-to-web flow.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.bodyMedium,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.colorScheme.onSurfaceVariant&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(32.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Button&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        onClick &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            scope.launch {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                startNativeLogin&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;fillMaxWidth&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(56.dp)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    ) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Login with PingOne&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.Waiting &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    CircularProgressIndicator&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.Error &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.headlineMedium&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(8.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;$state.message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.bodyMedium,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.colorScheme.onSurfaceVariant&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(8.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Button&lt;/span&gt;&lt;span&gt;(onClick &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { currentState &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; DaVinciState.Idle}) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Restart&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.ShowForm &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Secure Session Transfer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.headlineMedium&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(8.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Enter your login details.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.bodyMedium,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.colorScheme.onSurfaceVariant&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val collectors &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; state.collectors&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    // USERNAME&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val textCollector &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; collectors[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;] as TextCollector&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val usernameText &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        remember&lt;/span&gt;&lt;span&gt;() { &lt;/span&gt;&lt;span&gt;mutableStateOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TextFieldValue&lt;/span&gt;&lt;span&gt;(textCollector.value)) }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    TextField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; usernameText.value,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; modifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;wrapContentWidth&lt;/span&gt;&lt;span&gt;(Alignment.CenterHorizontally)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;fillMaxWidth&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;(top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 14.dp),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        shape &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; RoundedCornerShape&lt;/span&gt;&lt;span&gt;(8.dp),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        onValueChange &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            usernameText.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; it&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            textCollector.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; it.text&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        label &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; textCollector.label,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    // PASSWORD&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val passwordCollector &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; collectors[&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;] as PasswordCollector&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    val passwordText &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        remember { &lt;/span&gt;&lt;span&gt;mutableStateOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;TextFieldValue&lt;/span&gt;&lt;span&gt;(passwordCollector.value)) }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    TextField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; passwordText.value,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; modifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;wrapContentWidth&lt;/span&gt;&lt;span&gt;(Alignment.CenterHorizontally)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;fillMaxWidth&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;padding&lt;/span&gt;&lt;span&gt;(top &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 4.dp),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        onValueChange &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            passwordText.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; it&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            passwordCollector.value &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; it.text&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        label &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; passwordCollector.label&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        visualTransformation &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; PasswordVisualTransformation&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        keyboardOptions &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            KeyboardOptions&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                keyboardType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; KeyboardType.Password,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    // BUTTON&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Button&lt;/span&gt;&lt;span&gt;(onClick &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { scope.launch { &lt;/span&gt;&lt;span&gt;submitCreds&lt;/span&gt;&lt;span&gt;() } }) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Submit&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.Success &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Logged in&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.headlineMedium&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Hi $username&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.bodyMedium,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.colorScheme.onSurfaceVariant&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(32.dp))           &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                else&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code above should be “more or less” directly usable to build an example application, as long as you first &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/davinci/configuration.html&quot;&gt;follow the prerequisites required by the Ping SDK&lt;/a&gt;. It is, of course, provided with no warranties of any kind and must be used for your own education only. Right now, our app is pretty simple, as shown below.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;Extending the app to launch web content&lt;/h2&gt;
&lt;p&gt;Before we dive into the magic of the DaVinci flow that makes our final goal achievable, let’s extend the app code from above to launch the PingOne MyAccount web console in a browser view.&lt;/p&gt;
&lt;p&gt;First, we add a button to the UI by modifying the MainLayout compose function as below&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Composable&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fun &lt;/span&gt;&lt;span&gt;MainLayout&lt;/span&gt;&lt;span&gt;(modifier&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/* SNIP */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            when&lt;/span&gt;&lt;span&gt; (val state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; currentState) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/* SNIP */&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                is DaVinciState.Success &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Logged in&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.headlineMedium&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;Hi $username&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        style &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.typography.bodyMedium,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MaterialTheme.colorScheme.onSurfaceVariant&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Spacer&lt;/span&gt;&lt;span&gt;(modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier.&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(32.dp))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//Start of new code &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    Button&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        onClick &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            scope.launch {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                                launchUserProfileInBrowser&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        modifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Modifier&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;fillMaxWidth&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                            .&lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;(56.dp)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    ) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        Text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;View User Profile&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    } &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// End of new code &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                else&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we add a function to perform the PAR initiation and launch the browser with the received &lt;em&gt;request_uri&lt;/em&gt; value.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;suspend fun &lt;/span&gt;&lt;span&gt;launchUserProfileInBrowser&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val client &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; HttpClient&lt;/span&gt;&lt;span&gt;(Android) {}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val verifier &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; PkceGenerator.&lt;/span&gt;&lt;span&gt;generateVerifier&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val challenge &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; PkceGenerator.&lt;/span&gt;&lt;span&gt;generateChallenge&lt;/span&gt;&lt;span&gt;(verifier)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val myIP &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; getPublicIp&lt;/span&gt;&lt;span&gt;(client)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val requestTimestamp &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; System.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val expiresAt &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; requestTimestamp &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; 10000&lt;/span&gt;&lt;span&gt; // 10 seconds leeway&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val response&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; HttpResponse &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;submitForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            url &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &quot;https://auth.pingone.eu/&amp;lt;PingOne Environment ID&amp;gt;/as/par&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            formParameters &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; parameters {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;client_id&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;lt;PingOne Client ID for Session Transfer Application Client&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;response_type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;code&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;scope&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;openid web_session_only&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;redirect_uri&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;https://unused&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;request_token&quot;&lt;/span&gt;&lt;span&gt;, accessToken &lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;code_challenge&quot;&lt;/span&gt;&lt;span&gt;, challenge)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;code_challenge_method&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;S256&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;ip_address&quot;&lt;/span&gt;&lt;span&gt;, myIP&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;exp&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;$expiresAt&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                append&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;application_URL&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;https://apps.pingone.eu/&amp;lt;PingOne Environment ID&amp;gt;/myaccount/&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (response.status.&lt;/span&gt;&lt;span&gt;isSuccess&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            val responseBody &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.&lt;/span&gt;&lt;span&gt;bodyAsText&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            val jsonResponse &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JSONObject&lt;/span&gt;&lt;span&gt;(responseBody)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            val requestUri &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; jsonResponse.&lt;/span&gt;&lt;span&gt;getString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;request_uri&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            val authorizeUrl &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Uri.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://auth.pingone.eu/&amp;lt;PingOne Environment ID&amp;gt;/as/authorize&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;buildUpon&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;appendQueryParameter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;client_id&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&amp;lt;PingOne Client ID for Session Transfer Application Client&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;appendQueryParameter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;request_uri&quot;&lt;/span&gt;&lt;span&gt;, requestUri)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                .&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            val customTabsIntent &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; CustomTabsIntent.&lt;/span&gt;&lt;span&gt;Builder&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            customTabsIntent.&lt;/span&gt;&lt;span&gt;launchUrl&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;, authorizeUrl)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        } &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Handle HTTP errors (e.g. 400 Bad Request)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;PAR Error: ${response.status}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, we need a bit of utility code, as below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// A utility method to figure out our own IP&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;suspend fun &lt;/span&gt;&lt;span&gt;getPublicIp&lt;/span&gt;&lt;span&gt;(client&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; HttpClient)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; String&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            // Use a lightweight reflector&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            client.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://api.ipify.org&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;bodyAsText&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e: &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;//A utility object to help with PKCE&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;object PkceGenerator {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fun &lt;/span&gt;&lt;span&gt;generateVerifier&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; String {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val secureRandom &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; SecureRandom&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val code &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ByteArray&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        secureRandom.&lt;/span&gt;&lt;span&gt;nextBytes&lt;/span&gt;&lt;span&gt;(code)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; Base64.&lt;/span&gt;&lt;span&gt;encodeToString&lt;/span&gt;&lt;span&gt;(code, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fun &lt;/span&gt;&lt;span&gt;generateChallenge&lt;/span&gt;&lt;span&gt;(verifier&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; String)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; String {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val bytes &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; verifier.&lt;/span&gt;&lt;span&gt;toByteArray&lt;/span&gt;&lt;span&gt;(Charsets.US_ASCII)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val messageDigest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MessageDigest.&lt;/span&gt;&lt;span&gt;getInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;SHA-256&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        val digest &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; messageDigest.&lt;/span&gt;&lt;span&gt;digest&lt;/span&gt;&lt;span&gt;(bytes)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;span&gt; Base64.&lt;/span&gt;&lt;span&gt;encodeToString&lt;/span&gt;&lt;span&gt;(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What we have done above is to create a Pushed Authorization Request, where the following have been added as custom parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;request_token:&lt;/strong&gt; the user’s access token&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ip-address:&lt;/strong&gt; the current (external) IP address of the device&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exp:&lt;/strong&gt; a timestamp for 10 seconds from now&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;application_URL:&lt;/strong&gt; the URL of the actual application the user needs to be redirected to once the session has been created.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next is to craft a DaVinci flow that can use these inputs to deliver the desired outcome.&lt;/p&gt;
&lt;h2&gt;The second (and far more interesting) DaVinci flow&lt;/h2&gt;
&lt;p&gt;Before we dive into flow logic, we need to understand a bit about how PingOne SSO and DaVinci work together to handle pushed authorisation requests. In this case, because our Session Transfer Application client has been configured to require PAR and because it has the required flow linked via a DaVinci Policy, any PAR request (once validated to protocol correctness) &lt;a href=&quot;https://docs.pingidentity.com/davinci/integrating_flows_into_applications/davinci_launch_flow_redirect.html#referencing-pingone-data-in-the-flow&quot;&gt;will be passed to the DaVinci flow as a standard PingOne Flow invocation&lt;/a&gt;, with any custom parameters provided via the &lt;em&gt;{{global.parameters.authorizationRequest}}&lt;/em&gt; object. Hence to obtain the access token in the flow, we would use &lt;em&gt;{{global.parameters.authorizationRequest.request_token}}&lt;/em&gt; and so on.&lt;/p&gt;
&lt;p&gt;Our example flow then looks as follows:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;The steps are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check expiry of the request.&lt;/strong&gt; This is done by using a Function node to compare the exp claim in the authorisation request to the current timestamp. Note that this check is perhaps not entirely necessary, since the PAR functionality in Ping One already constrains the validity of the request_uri, but it is included here as just one of any number of additional security checks that could be added to the flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check IP address:&lt;/strong&gt; Again we use a function node to check the current IP address as seen by DaVinci against the address that was included in the PAR payload.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Validate request token:&lt;/strong&gt; this is the critical step where we ensure that the access token passed in the PAR payload is valid before extracting the token subject to create a session. In this example, I am using DaVinci’s Token Management -&amp;gt; Validate JWT Token capability and using the PngOne’s environment’s JWKS endpoint to provide the public key for validation. An alternative approach would be to make a REST call to the token introspection endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create a (web) session for the user:&lt;/strong&gt; having ensured that a valid token has been provided, we are free to create the necessary session, represented by the PingOne ST cookie in the user’s browser. The simplest way to do this is to use the PingOne Authentication -&amp;gt; Create or Update Session capability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redirect to the target application:&lt;/strong&gt; The final step is to generate a redirect so that the actual application (in our example the MyAccount app) can start its own OIDC flow to transparently log the user in based on the session cookie we just created. I’m using a simple Custom HTML template node in DaVinci with a Javascript redirect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;A final test&lt;/h2&gt;
&lt;p&gt;We should have everything we need and our application experience now looks as follows:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;In our final episode…&lt;/h2&gt;
&lt;p&gt;In the next article, I will build on this approach and look at a number of advanced techniques we could use to make this hand-off process even more secure.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CiXag4ms_27rSQ8.webp" medium="image" type="image/webp" width="2386" height="1256"/></item><item><title>Ping Orchestration SDKs 2.0</title><link>https://developer.pingidentity.com/blog/ping-orchestration-sdks-2-0/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/ping-orchestration-sdks-2-0/</guid><description>One SDK family for DaVinci, AIC/PingAM journeys, and OIDC across web and native. Here&apos;s what changed, what it means for your apps, and how to get started.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ping Orchestration SDKs 2.0 are our next generation of client-side SDKs for JavaScript, iOS, and Android! They give you one unified way to connect your apps to PingOne DaVinci flows, PingOne Advanced Identity Cloud / PingAM journeys, and OIDC redirect with any OIDC-compliant server. You integrate once, then evolve journeys and flows on the server without constantly rewriting your app.&lt;/p&gt;
&lt;p&gt;Want to get started now? Jump to &lt;a href=&quot;#getting-started&quot;&gt;Getting Started&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;Key features&lt;/h2&gt;
&lt;p&gt;The SDKs handle multi-client orchestration for you. A single app can work with multiple OAuth clients simultaneously, such as one for primary login and another for step-up or high-risk actions. The SDK manages tokens and clients so your code can focus on business logic rather than OAuth details.&lt;/p&gt;
&lt;p&gt;They also integrate Ping’s universal services through the orchestration layer. The SDK can collect device and behavioral context (for example, with PingOne Protect) and feed that into your DaVinci flows or AIC/PingAM journeys so you can drive risk-based decisions from a single place. As we add more services, like PingOne MFA for PingOne or privacy-preserving biometrics with PingOne Keyless, they appear through the same orchestration model instead of new one-off integrations.&lt;/p&gt;
&lt;p&gt;Across JavaScript, Android, and iOS, the orchestration model is consistent. Teams can reuse patterns across web and native apps, and new developers can ramp up faster because the concepts look and feel the same on each platform.&lt;/p&gt;
&lt;h2&gt;Why Orchestration 2.0?&lt;/h2&gt;
&lt;p&gt;Historically, DaVinci flows and AIC/PingAM journeys had separate SDK stories. Orchestration 2.0 brings them together into one orchestration-first family across JS, Android, and iOS.&lt;/p&gt;
&lt;p&gt;The older ForgeRock SDKs evolved into several large libraries. Orchestration 2.0 replaces that with a modular, pluggable design with networking, journeys, FIDO/passkeys, MFA, device binding, device ID/profiling, and more, so you only include what you need and stay aligned with modern Kotlin, Swift, and JS practices. This single orchestration plane is where we focus on performance, security, and new capabilities.&lt;/p&gt;
&lt;h2&gt;Benefits vs. ForgeRock SDKs&lt;/h2&gt;
&lt;p&gt;If you use the ForgeRock JS, Android, or iOS SDKs today, Orchestration 2.0 gives you one SDK family for both DaVinci and journeys. The same core ideas apply whether you’re building SPAs or native apps with AIC/PingAM.&lt;/p&gt;
&lt;p&gt;The architecture is more modular: journey orchestration, FIDO2/passkeys, OATH and Push MFA, device binding, migration helpers, device ID/profiling, jailbreak/root detection, and reCAPTCHA Enterprise are all focused modules. You import only what you need, which helps with footprint and security. The orchestration model also adds built-in multi-client support, so scenarios like primary login plus step-up are handled by the SDK instead of custom token juggling. Everything sits on modern JavaScript, Android, and iOS foundations.&lt;/p&gt;
&lt;h2&gt;Why new SDKs vs. evolving ForgeRock SDKs&lt;/h2&gt;
&lt;p&gt;The ForgeRock SDKs assumed separate paths for DaVinci and AIC/PingAM and concentrated a lot of behavior into a small number of libraries. That made it hard to keep adding features, support richer token patterns, and plug in new services without extra complexity.&lt;/p&gt;
&lt;p&gt;With Orchestration 2.0, we refactored orchestration into a new architecture, split cross-cutting concerns (networking, storage, device binding, FIDO, MFA, and so on) into modules, and made these SDKs the contract for all new integrations and universal services. The goal is that app development feels simpler and more consistent, even as the platform becomes more powerful behind the scenes.&lt;/p&gt;
&lt;h2&gt;If you use ForgeRock SDKs with PingOne AIC/PingAM&lt;/h2&gt;
&lt;p&gt;The ForgeRock JS, Android, and iOS SDKs are now deprecated. They’ll receive critical security and bug fixes for about two years after Orchestration 2.0 GA, until 15 April 2028. After that, they won’t be updated, and the expectation is that customers move to the Orchestration SDKs.&lt;/p&gt;
&lt;p&gt;For most AIC/PingAM customers, treat migration as a structured project.
Start with the &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/migration.html&quot;&gt;detailed guides&lt;/a&gt;
to understand
how to migrate from the ForgeRock SDKs to the Ping Orchestration SDKs.
These guides provide concrete examples
of how to migrate existing ForgeRock integrations to the new SDKs
and make it easier to map journey callbacks,
device behavior, MFA flows such as OATH, and Push.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use AI Coding Agents
(GitHub Copilot, Cursor, Claude Code, etc) to expedite migration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context-ready&lt;/strong&gt;: We’ve included structured &lt;code&gt;migration.md&lt;/code&gt; files for the &lt;a href=&quot;https://github.com/ForgeRock/ping-android-sdk/blob/master/journey/migration.md&quot;&gt;&lt;code&gt;ping-android-sdk&lt;/code&gt;&lt;/a&gt; and the
&lt;a href=&quot;https://github.com/ForgeRock/ping-ios-sdk/blob/master/Journey/migration.md&quot;&gt;&lt;code&gt;ping-ios-sdk&lt;/code&gt;&lt;/a&gt;. You can feed these files directly into your AI’s context to help it automatically map old ForgeRock classes to the new Orchestration 2.0 modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated Refactoring&lt;/strong&gt;: Use prompts to help the agent rewrite your networking and journey logic, significantly reducing the manual effort of updating callback handlers and device binding code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coming Soon:&lt;/strong&gt; We are currently developing a dedicated We now have Agent Skills will act as a migration specialist, providing real-time, project-specific guidance for moving your unique identity flows into the new orchestration plane. Refer to our &lt;a href=&quot;https://github.com/pingidentity/ping-sdk-agent-skills&quot;&gt;ping-sdk-agent-skills&lt;/a&gt; repo or our &lt;a href=&quot;https://developer.pingidentity.com/build-with-ai/&quot;&gt;build with AI&lt;/a&gt; documentation for more information.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A common pattern is to run the new and legacy SDKs side by side. The Orchestration SDKs can coexist with the old ones, and dedicated migration modules for device binding, device ID, and OATH/Push (plus encrypted state helpers) are there to migrate identifiers without forcing most users to re-enroll. After you’re confident in the new model, you initialize the Orchestration SDKs in your apps, move UI and callback handling to the new patterns, and keep most behavior in AIC/PingAM journeys so you can keep iterating without constant app releases.&lt;/p&gt;
&lt;p&gt;A typical migration has three phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with inventory and design: list all apps and journeys using the ForgeRock SDKs, classify the flows they rely on: sign on, registration, self-service, MFA, device binding, passkeys, and so on, and call out any reliance on Transactional Authorization or the Token Vault.&lt;/li&gt;
&lt;li&gt;From there, you run a pilot on one representative mobile or web app. You integrate the Orchestration SDKs in a feature branch, use the migration guides and sample apps (with or without AI assistance) to map journeys to the new orchestration model, and validate device binding and device ID migration in a non-production environment.&lt;/li&gt;
&lt;li&gt;After you feel confident, you move into rollout: promote the pilot to production, extend the new SDKs to other apps and platforms, and then remove the legacy SDK dependencies when you have full coverage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the Orchestration 2.0 release, Transactional Authorization and the JavaScript Token Vault are not included. If you rely on either in production, contact your Ping representative and open a support case so we can work through options with you rather than attempting a direct, like-for-like migration.&lt;/p&gt;
&lt;h2&gt;Docs, Login Widget, and resources&lt;/h2&gt;
&lt;p&gt;DaVinci SDK docs have been folded into the Orchestration SDK documentation. DaVinci-focused guides and tutorials now live there, and older DaVinci-only pages will redirect or point you to the new docs. If you used the “DaVinci module” docs before, the Orchestration SDK JavaScript, Android, and iOS docs are now your primary reference.&lt;/p&gt;
&lt;p&gt;The Login Widget now lives in its own &lt;a href=&quot;https://developer.pingidentity.com/login-widget/index.html&quot;&gt;repository&lt;/a&gt;,
allowing it
to evolve independently while remaining part of the orchestration story.
It is referenced from both the deprecated ForgeRock SDK docs and the Orchestration SDK docs as one of the available integration options alongside a fully custom UI.&lt;/p&gt;
&lt;p&gt;Sample applications for JavaScript, Android, and iOS SDK and the SDK repositories and packages are all &lt;a href=&quot;https://developer.pingidentity.com/sdks.html&quot;&gt;linked&lt;/a&gt; from the Ping Developer Portal. The portal acts as the central entry point for Orchestration SDK documentation, API references, tutorials, so you can find everything in one place.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;To get started with the Orchestration SDKs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reference our tutorials for &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/davinci/tutorials.html&quot;&gt;DaVinci&lt;/a&gt; or &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/journey/try-it-out.html&quot;&gt;AIC/PingAM&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Clone our SDK &lt;a href=&quot;https://github.com/ForgeRock/sdk-sample-apps&quot;&gt;sample apps&lt;/a&gt; repository and enter relevant server details to understand the fundamentals and walk through core orchestration flows end to end, so you can compare a working reference to your current setup before touching production or wondering how to integrate authentication flows into your existing mobile and web apps.&lt;/li&gt;
&lt;li&gt;Integrate into your existing app.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The SDKs are available from standard locations: JavaScript on GitHub and NPM, Android on GitHub and Maven, and iOS on GitHub and Swift Package Manager. The Ping Developer Portal will be the main entry point, bringing together orchestration docs, sample apps, and API references in one place.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Note:&lt;/strong&gt; Soon you’ll see We now have AI-powered Agent Skills and tools to help integrate into existing apps or set up sample apps quickly at our &lt;a href=&quot;https://github.com/pingidentity/ping-sdk-agent-skills&quot;&gt;ping-sdk-agent-skills&lt;/a&gt; repo.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SDK Sample applications: &lt;a href=&quot;https://github.com/ForgeRock/sdk-sample-apps&quot;&gt;https://github.com/ForgeRock/sdk-sample-apps&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDK repositories:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iOS: &lt;a href=&quot;https://github.com/ForgeRock/ping-ios-sdk/tree/master&quot;&gt;https://github.com/ForgeRock/ping-ios-sdk/tree/master&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android: &lt;a href=&quot;https://github.com/ForgeRock/ping-android-sdk/tree/master&quot;&gt;https://github.com/ForgeRock/ping-android-sdk/tree/master&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS: &lt;a href=&quot;https://github.com/ForgeRock/ping-javascript-sdk/tree/main&quot;&gt;https://github.com/ForgeRock/ping-javascript-sdk/tree/main&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All Ping Identity’s SDKs: &lt;a href=&quot;https://developer.pingidentity.com/sdks.html&quot;&gt;https://developer.pingidentity.com/sdks.html&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orchestration SDK Documentation: &lt;a href=&quot;https://developer.pingidentity.com/orchsdks/index.html&quot;&gt;https://developer.pingidentity.com/orchsdks/index.html&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Advanced Identity Cloud/PingAM Login Widget: &lt;a href=&quot;https://developer.pingidentity.com/login-widget/index.html&quot;&gt;https://developer.pingidentity.com/login-widget/index.html&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecated ForgeRock SDK Documentation: &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/index.html&quot;&gt;https://docs.pingidentity.com/sdks/latest/index.html&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.5uUWCk_D_ZlpQmJ.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Understanding Virtual Properties</title><link>https://developer.pingidentity.com/blog/understanding-virtual-properties/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/understanding-virtual-properties/</guid><description>Learn how to configure Virtual Properties and Relationship-Derived Virtual Properties (RDVPs) in PingIDM and PingOne Advanced Identity Cloud.</description><pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://gwizkid.com/posts/understanding-vps-rdvps/&quot;&gt;Gwiz Kid&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;PingOne Advanced Identity Cloud (AIC) and PingIDM (IDM) contain an impressive range of flexible identity configuration schema that allow you to define complex relationships and dynamic concepts given the data inside your directory. And if that sentence sounded complicated, it’s because it is: relationships are hard enough in the real world, let alone when configuring it in an enterprise identity management system!&lt;/p&gt;
&lt;p&gt;This Guide will walk you through the concepts of Virtual Properties and Relationship-Derived Virtual Properties (otherwise known as RDVPs). It’s expected that you are comfortable with navigating the identity management console in PingIDM or AIC and have a reasonable understanding of managing properties within that console. While this Guide covers RDVPs, it is not a primer on Relationships in either platform.&lt;/p&gt;
&lt;p&gt;This Guide is broken down into the following sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#virtual-properties&quot;&gt;Virtual Properties&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#relationship-derived-virtual-properties-rdvps&quot;&gt;Relationship-Derived Virtual Properties&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By the end of this Guide you will have gained an understanding on where and when to use each virtual property type as well as how to configure those properties. You will also have configured a set of simple examples illustrating the capabilities of those properties.&lt;/p&gt;
&lt;h2&gt;Virtual Properties&lt;/h2&gt;
&lt;p&gt;A &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/idm-objects/managed-object-virtual-properties.html&quot;&gt;Virtual Property&lt;/a&gt; is &lt;strong&gt;a Property that can dynamically calculate its own data&lt;/strong&gt;. Rather than having a separate sub-process generate and store a value, a Virtual Property handles the dirty work for you directly on the property itself.&lt;/p&gt;
&lt;p&gt;A great example of when a Virtual Property can be used is a user’s full name. We could have a function in our client app(s) that pulls in the user’s Given Name and Surname and then merges them together, but we might run into consistency issues on how that name is displayed. If we are provisioning users to an application, we’d have to repeat our work again to ensure that our target app gets the same value. Rather, with a Virtual Property we can define a “Full Name” value that ensures that the name is consistent each and every time it needs to be referenced.&lt;/p&gt;
&lt;p&gt;But it’s more than just combining values together. Virtual Properties have access to IDM’s scripting engine, meaning that you can return whatever values you deem necessary to be retrieved alongside your object - even if the value doesn’t live on that object! As an example, let’s say you’re a retailer that uses a third-party service to calculate and store a user’s clothing sizes. Since that service is holding the sizing information, why not just reach out via API to pull in the user’s size dynamically? No need to create a polling schedule, no need to store that value both in your system and in theirs - the data is in one spot but can be returned in more than one type of request.&lt;/p&gt;
&lt;h2&gt;Creating a Virtual Property&lt;/h2&gt;
&lt;p&gt;We will walk through specific examples of virtual properties below, but the big thing you need to know when defining a Virtual Property is the &lt;strong&gt;Virtual Property Setting&lt;/strong&gt;, &lt;strong&gt;Return by Default&lt;/strong&gt;, and &lt;strong&gt;onRetrieve Scripting&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To make an element a Virtual Property, enable the “Virtual Property” option under the “Show Advanced Options” section on the property’s main page.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating a Virtual Property&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enabling this checkbox does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It informs IDM that this property is a virtual property and as a result cannot be modified directly by the user. Whether or not you enable “User Editable” in the advanced options will not change this behavior.&lt;/li&gt;
&lt;li&gt;It enables and displays the “Return by Default” checkbox, which lets you specify if this property should show up as a default field when querying the object (kind of like how the &lt;code&gt;_id&lt;/code&gt; property shows up in every query). For most cases, &lt;strong&gt;enable Return by Default&lt;/strong&gt;. If you don’t, the virtual property won’t show up throughout your platform UI (which is expecting static values) or your application unless explicitly requested.&lt;/li&gt;
&lt;li&gt;It enables and displays the “Query” tab, which lets you specify queries to interact with relationships and define &lt;a href=&quot;#relationship-derived-virtual-properties-rdvps&quot;&gt;Relationship-Derived Virtual Properties&lt;/a&gt; - more on that later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now that your property is defined as being a Virtual Property, to make it useful you’ll want to define an &lt;strong&gt;onRetrieve script&lt;/strong&gt; that auto-calculates the dynamic data. The scripts for your property are located in the “Scripts” tab.&lt;/p&gt;
&lt;h3&gt;What Makes a Virtual Property a Virtual Property?&lt;/h3&gt;
&lt;p&gt;You’ve probably noticed that your properties have two more scripts available - &lt;strong&gt;onValidate&lt;/strong&gt; and &lt;strong&gt;onStore&lt;/strong&gt;. In fact, all three script types are available (including onRetrieve) whether or not you enabled this element to be a Virtual Property. What gives?&lt;/p&gt;
&lt;p&gt;Virtual Properties in IDM are special in that they &lt;strong&gt;cannot be edited and can be returned by default&lt;/strong&gt;. Regular properties, while able to be dynamically updated, can still be configured to be edited and have to be specified to be returned.&lt;/p&gt;
&lt;p&gt;Virtual properties can also use onValidate and onStore scripts when defining their data. But what makes Virtual Properties interesting is that they can &lt;strong&gt;dynamically calculate data at time of retrieval WITHOUT having to store that information&lt;/strong&gt;. By making a property virtual, you are indicating that this data will always be available and up to date when requested.&lt;/p&gt;
&lt;p&gt;So as a recap: Virtual Properties give you a way to define a property that is calculated and returned at run-time and that your system recognizes will always be dynamically calculated. When creating a  Virtual Property, you’re probably using the onRetrieve script because then you aren’t storing a value that could easily fall out of date.&lt;/p&gt;
&lt;h2&gt;Defining the onRetrieve Script&lt;/h2&gt;
&lt;p&gt;The onRetrieve script lets you return a value when requested. &lt;strong&gt;The returned value is not stored unless explicitly provided when updating the object&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;PingIDM supports JavaScript and Groovy for scripting. However, since JavaScript is available in both AIC and PingIDM, our examples in this guide will stick with JavaScript only.&lt;/p&gt;
&lt;p&gt;There are two main documents you should be referencing when working with scripts in IDM:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/idm-scripting/script-triggers-managedConfig.html&quot;&gt;Script Triggers &amp;amp; Variables&lt;/a&gt;: This page will tell you for each script type what default variables exist within the script itself.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/idm-scripting/scripting-func-engine.html&quot;&gt;Script Functions&lt;/a&gt;: This page will define the types of functions that extend capabilities, such as encryption/decryption, interacting with identity resources, or performing REST calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most important takeaways from those documents are found in the below sections.&lt;/p&gt;
&lt;h3&gt;IDM Scripting Variables&lt;/h3&gt;
&lt;p&gt;It’s easy to miss, but the documentation for onRetrieve actually exists in two parts of the Script Triggers page: the &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/idm-scripting/script-triggers-managedConfig.html#managed_object_configuration_object&quot;&gt;Managed Configuration Object&lt;/a&gt; and in the &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/idm-scripting/script-triggers-managedConfig.html#property_object&quot;&gt;Property Object&lt;/a&gt;. In the case of a Virtual Property, we care about the Property Object details.&lt;/p&gt;
&lt;p&gt;There are two variables in particular that you’ll likely deal with the most: &lt;strong&gt;object&lt;/strong&gt; and &lt;strong&gt;property&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;object&lt;/strong&gt; variable is the object that is being requested. That means your Virtual Property can request, via object notation, other properties from your object to be used within the Virtual Property. Think, for example, referencing a user’s student ID from their profile to retrieve a list of library books they’ve checked out in an external library service.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;property&lt;/strong&gt; variable is the property you’re returning. &lt;strong&gt;The property variable is required in an onRetrieve script&lt;/strong&gt; if you want to return data. To use, set &lt;code&gt;property =&lt;/code&gt; whatever variable/value you’ve created &lt;strong&gt;at the end of your script&lt;/strong&gt; and you’re done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The predefined variables are protected&lt;/strong&gt;. If you create a variable with the same name your script won’t work as expected.&lt;/p&gt;
&lt;h2&gt;IDM Scripting Functions&lt;/h2&gt;
&lt;p&gt;There are a good number of script functions available to you, but the following in particular will likely be your go-tos:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;openidm.query()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;openidm.query&lt;/code&gt; queries resource objects along with a query filter. Resource objects can be managed objects or the underlying system configuration. There may be use cases in which you need to pull data that exists off of another resource based on data that you have on your existing object and the query function lets you get that data to be dynamically returned in your Virtual Property.&lt;/p&gt;
&lt;p&gt;As an example, let’s say that you wanted a list of all of the child organizations that you are a member of (as a result of being a member of the parent organization). You could query the &lt;code&gt;organizations&lt;/code&gt; managed object to get those IDs based on a property you have on your user.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; query &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.memberOfOrgIDs.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;orgID&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; `parentIDs eq &apos;${&lt;/span&gt;&lt;span&gt;orgID&lt;/span&gt;&lt;span&gt;}&apos;`&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos; or &apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; myChildOrgs &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; openidm.&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;managed/alpha_organization&quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;span&gt;&quot;_queryFilter&quot;&lt;/span&gt;&lt;span&gt;: query}).result;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;openidm.action()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;openidm.action&lt;/code&gt; performs HTTP actions on a particular resource. This could be an action on something in AIC, say sending an email template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; emailParams &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;templateName&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;welcome&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;to&quot;&lt;/span&gt;&lt;span&gt; : user.mail,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;cc&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;ccUser1@example.com,ccUser2@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;bcc&quot;&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;&quot;bigBoss@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;object&quot;&lt;/span&gt;&lt;span&gt; : {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;this&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;that&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;another value&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;openidm.&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;external/email&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;sendTemplate&quot;&lt;/span&gt;&lt;span&gt;,  emailParams);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it could also be interacting with custom IDM endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;openidm.&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;endpoint/{yourEndpointName}&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;yourEndpointAction&quot;&lt;/span&gt;&lt;span&gt;, params);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As well as interacting with external REST APIs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; convertObjectToQueryParams&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; Object.&lt;/span&gt;&lt;span&gt;keys&lt;/span&gt;&lt;span&gt;(object)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; encodeURIComponent&lt;/span&gt;&lt;span&gt;(key) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &apos;=&apos;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; encodeURIComponent&lt;/span&gt;&lt;span&gt;(object[key]))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&amp;amp;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; body &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;foo&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;bar&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; params &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  url: &lt;/span&gt;&lt;span&gt;`https://my-endpoint-example.example`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  body: &lt;/span&gt;&lt;span&gt;convertObjectToQueryParams&lt;/span&gt;&lt;span&gt;(body),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  authenticate: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    type: &lt;/span&gt;&lt;span&gt;&apos;bearer&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    token: token&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; openidm.&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;external/rest&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;call&apos;&lt;/span&gt;&lt;span&gt;, params);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are plenty of other use cases for &lt;code&gt;openidm.action&lt;/code&gt; but the above will serve you well when working with Virtual Properties.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;identityServer.getProperty()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This one is a bit sneaky because it’s most useful in AIC and doesn’t appear in the aforementioned documentation. &lt;code&gt;identityServer.getProperty(esvName)&lt;/code&gt; lets you reference ESVs stored in Google Secrets Manager within your scripts. Just remember - your ESVs are referenced with &lt;strong&gt;dots&lt;/strong&gt; and not dashes, so the ESV you created with the name &lt;code&gt;esv-my-cool-esv&lt;/code&gt; would be referenced as &lt;code&gt;identityServer.getProperty(&apos;esv.my.cool.esv&apos;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As always with ESVs, be careful not to leak sensitive data in your scripting.&lt;/p&gt;
&lt;p&gt;Now that we’ve gotten a sense of Virtual Properties, let’s make a couple examples and test them out!&lt;/p&gt;
&lt;h2&gt;Virtual Property Examples&lt;/h2&gt;
&lt;p&gt;If you’re using PingOne Advanced Identity Cloud (e.g. AIC), head to the Native IDM Console underneath Native Consoles → Identity Management. If you are using PingIDM, you’re already where you need to be.&lt;/p&gt;
&lt;p&gt;In our example we will be updating our User object (in AIC, that’ll be called &lt;code&gt;alpha_user&lt;/code&gt;) to return the “Full Name” attribute we discussed above. In the Managed Objects page (normally your homepage, but also under Configure → Managed Objects) click on your User object. The property editor for your user should appear along with all of the pre-defined properties that exist on that user.&lt;/p&gt;
&lt;p&gt;Click the “+ Add a Property” button and add a new property with the &lt;strong&gt;name&lt;/strong&gt; &lt;code&gt;custom_fullName&lt;/code&gt;, the &lt;strong&gt;label&lt;/strong&gt; “Full Name”, and the &lt;strong&gt;type&lt;/strong&gt; &lt;code&gt;String&lt;/code&gt; and then hit “Save”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Creating the Full Name Property&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click on your new property to edit it.&lt;/p&gt;
&lt;p&gt;At this point, we are dealing with a standard String property. You can enter in a full name value and it’ll store that value. What we want is a property that dynamically updates based on the details of the &lt;code&gt;givenName&lt;/code&gt; and &lt;code&gt;sn&lt;/code&gt; of our user.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Generic Full Name String Property&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click on “Show advanced options”, uncheck “User Editable”, and check “Virtual”. You’ll notice that an additional property, “Return by Default” appears. Check the “Return by Default” property if you want the full name to show up in UIs (like the end-user dashboard and the administrative UI), by default in API calls, or if you want this property to be fetched in provisioners - otherwise you may not have access to the result.&lt;/p&gt;
&lt;p&gt;In our case we are going to check “Return by Default” since we will want to see the value in the UI.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Advanced Configuration&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Save the changes. When you do, something interesting happens: A &lt;strong&gt;Query Configuration&lt;/strong&gt; tab appears at the top of the property. We’ll get back to that later - right now, what we care about is the &lt;strong&gt;Scripts&lt;/strong&gt; tab.&lt;/p&gt;
&lt;p&gt;Let’s define the script that makes our Virtual Property tick.&lt;/p&gt;
&lt;h3&gt;Example: Manipulating Existing Properties&lt;/h3&gt;
&lt;p&gt;Click on the Scripts tab, select the &lt;code&gt;onRetrieve&lt;/code&gt; script type and click “+ Add Script”. A Script Manager window will appear with the ability to select a script type, provide an inline script or a file, and pass variables into that script. If you’re using AIC, you’ll only have access to JavaScript and the Inline Script option (don’t worry, you can still pass variables too) - in PingIDM, you can pick and choose what you prefer.&lt;/p&gt;
&lt;p&gt;Your inline script should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.givenName &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.sn;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hit Save.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To set your property variable, &lt;strong&gt;you must put the property definition at the end of your script.&lt;/strong&gt; Otherwise, the value will not be set. If you are setting property in more than one location in your script, you can alternatively end the script with just the value &lt;code&gt;property&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Create a user with a First and Last name and then take a look at their properties. You should see the concatenated First and Last name.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Full Name Virtual Property&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now this script by itself isn’t particularly exciting. In fact, the &lt;code&gt;cn&lt;/code&gt; property on the user already has this information. Why don’t we change this property to update the &lt;em&gt;order&lt;/em&gt; of the First and Last name given the country they reside in, based on common ordering in those areas?&lt;/p&gt;
&lt;p&gt;Updated your onRetrieve function to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; givenFamilyNameOrderCountries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;US&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;ES&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;AU&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;CA&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;GB&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; familyGivenOrderCountries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;FR&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;JP&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;CN&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;IN&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (givenFamilyNameOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(object.country)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.givenName &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.sn;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; if&lt;/span&gt;&lt;span&gt; (familyGivenOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(object.country)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.sn &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.givenName;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.givenName &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.sn;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On your user, change your country to &lt;code&gt;US&lt;/code&gt; and then to &lt;code&gt;IN&lt;/code&gt;. You’ll see that the order of your first and last name flips.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Full Name Virtual Property, Surname First&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, before any of you send me a strongly-worded email:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is an &lt;strong&gt;example&lt;/strong&gt;. It’s not how you should be determining name order - ideally, other information like the user’s language preference and possibly a boolean on their profile could help determine the positioning of a person’s name.&lt;/li&gt;
&lt;li&gt;Yes, we have variables. But then you would have had to have typed each of those country codes one at a time into the variable array(s) you created. I just saved you 5 minutes of time and reduced your risk of carpal tunnel syndrome.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That all being said, onto a more interesting use case.&lt;/p&gt;
&lt;h3&gt;Example: Returning Data from External APIs&lt;/h3&gt;
&lt;p&gt;Now that we have the capability to get data based on attributes, let’s pass some of that data into an external REST API. Here’s where we really get into the good stuff Virtual Properties have to offer: if the data you need lives elsewhere, and you don’t really want that data inside your directory (AIC or PingDS or otherwise), Virtual Properties let you get that data when you need it without having to set up and perform any syncing operations. Your data is always up to date because it is asking for the data from the source.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A Note On Performance&lt;/strong&gt;: Consider the implications of reaching out to an external API if you are making many read operations on your data. If the frequency as to which you’re getting this property far exceeds the frequency that data is updated, you should consider updating the property via a &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/identities/manage-scheduled-jobs.html&quot;&gt;scheduled job&lt;/a&gt; or onStore trigger instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We’re going to update our virtual property to guess the country of the user based on their first name if their country code was not provided. Then, that country can be used in the same way as above to determine the user’s name order.&lt;/p&gt;
&lt;p&gt;We’ll use the free &lt;a href=&quot;https://nationalize.io&quot;&gt;nationalize.io&lt;/a&gt; API to do this. Update your onRetrieve script to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; givenFamilyNameOrderCountries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;US&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;ES&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;AU&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;CA&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;GB&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; familyGivenOrderCountries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;FR&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;JP&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;CN&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;IN&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; country &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.country;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (country &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; country &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; params &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        url: &lt;/span&gt;&lt;span&gt;`https://api.nationalize.io/?name=${&lt;/span&gt;&lt;span&gt;encodeURIComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;givenName&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        method: &lt;/span&gt;&lt;span&gt;&apos;GET&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; openidm.&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;external/rest&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;call&apos;&lt;/span&gt;&lt;span&gt;, params);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; sorted_countries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.country.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; b.probability &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; a.probability)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    var&lt;/span&gt;&lt;span&gt; filtered_countries &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; sorted_countries&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        .&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; givenFamilyNameOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(c.country_id) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; familyGivenOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(c.country_id));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (filtered_countries.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        country &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; filtered_countries[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;].country_id;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (givenFamilyNameOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(country)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.givenName &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.sn;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; if&lt;/span&gt;&lt;span&gt; (familyGivenOrderCountries.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(country)) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.sn &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.givenName;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    property &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; object.givenName &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &quot; &quot;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; object.sn;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, testing with common names from different countries, you should see the name order position accordingly. Feel free to test with the name combinations below to see for yourself - remembering to remove the Country code for the HTTP call to be made.&lt;/p&gt;
&lt;div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expected Order&lt;/th&gt;
&lt;th&gt;Given Name&lt;/th&gt;
&lt;th&gt;Surname&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Surname, Given Name&lt;/td&gt;
&lt;td&gt;Ashok&lt;/td&gt;
&lt;td&gt;Kumar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Given Name, Surname&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;Doe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Relationship-Derived Virtual Properties (RDVPs)&lt;/h2&gt;
&lt;p&gt;Virtual Properties can be updated either &lt;strong&gt;on time of retrieval&lt;/strong&gt; or &lt;strong&gt;based on notifications received from a relationship&lt;/strong&gt; tied to the managed object this property lives in. When we are updating the property based on information provided by the relationship, we are working with a &lt;strong&gt;Relationship-Derived Virtual Property, or RDVP&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A Relationship-Derived Virtual Property updates based on notifications from the Relationships linked to its Managed Object. An RDVP has access to content from the Object’s Relationships and can traverse the entire Relationship path to create a value composed from an aggregate of that traversal.&lt;/p&gt;
&lt;p&gt;Let’s look at an example of when an RDVP might be useful - a management chain.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;A Simple Management Diagram&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the diagram above, Joe is the CEO and the top of the management hierarchy. Jess is one of his direct reports, and Jim lives at the bottom of the hierarchy underneath another of Joe’s reports.&lt;/p&gt;
&lt;p&gt;This hierarchy is defined by &lt;strong&gt;relationships&lt;/strong&gt; - the linkages between each manager and their reports.&lt;/p&gt;
&lt;p&gt;If you wanted to get a list of all of the Managers above an Employee, how would you go about it? Normally, it would likely be a series of queries that would look something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the Employee&lt;/li&gt;
&lt;li&gt;Find the Employee’s Manager&lt;/li&gt;
&lt;li&gt;Get the Manager&lt;/li&gt;
&lt;li&gt;Find the Manager’s Manager&lt;/li&gt;
&lt;li&gt;Get the Manager’s Manager&lt;/li&gt;
&lt;li&gt;Find the Manager’s Manager’s Manager&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;li&gt;Manager X has no Manager, return&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think about this the other way, too. If I wanted to get all of the Employees under a Manager, and each Manager has more than one Employee, how many operations do I have to take? Now what if I had to perform this action every time I entered an application, or every time I pull data on a user? This is starting to sound a lot like one of those &lt;a href=&quot;https://www.geeksforgeeks.org/dsa/top-50-graph-coding-problems-for-interviews/&quot;&gt;classic graph traversal interview questions&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The deeper or wider you go, with the more connections you have, the more operations you would normally have to take. This makes your code complex and significantly increases the number of queries you need to make to get the data you and your users need to be successful. RDVPs solve this problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDVPs travel the Relationships for you and update a static result on the object directly at time of change from anywhere in the Relationship.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If we looked at the previous example, if I wanted to get a list of the Employee’s Managers, or a list of the Manager’s Employees, the data is on the User directly. If the Employee is promoted, or switches teams, and the managerial hierarchy changes, &lt;strong&gt;the data on the User updates automatically&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;When to Use RDVPs&lt;/h2&gt;
&lt;p&gt;RDVPs update when the data updates in the relationship, anywhere in the relationship(s) you are getting the data from.&lt;/p&gt;
&lt;p&gt;There’s a time and a place to use RDVPs as a result:&lt;/p&gt;
&lt;div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If the data updates&lt;/th&gt;
&lt;th&gt;…and you want to get that data&lt;/th&gt;
&lt;th&gt;…then you should use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frequently&lt;/td&gt;
&lt;td&gt;Infrequently&lt;/td&gt;
&lt;td&gt;A Virtual Property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrequently&lt;/td&gt;
&lt;td&gt;Frequently&lt;/td&gt;
&lt;td&gt;An RDVP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;RDVPs are triggered on &lt;a href=&quot;#notifications&quot;&gt;relationship notifications&lt;/a&gt; and can trigger additional notifications as a result of them updating. If you’re not careful, these notifications can get rather noisy - one change could result in hundreds (if not hundreds of thousands) of changes cascading through your directory. The tradeoff is that you aren’t making that same calculation at time of retrieval - the likely point in which your enforcement point or authorization server is attempting to make a real-time decision based on that information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDVPs should only traverse a relationship tree in one direction&lt;/strong&gt;. Cyclical RDVPs could result in cyclical updates that end in slowdown, or potentially takedown, of your entire system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDVPs are stored&lt;/strong&gt;. They are only updated when the data they’re related to updates. If your relationship notifications do not inform the RDVP to change, they will not change. This is a good and a bad thing - it means you have a static representation of your relationship data, but it also means that representation is a snapshot of what the data was &lt;strong&gt;when the notification was triggered&lt;/strong&gt;. A newly-configured RDVP won’t have any data in it until the linked relationship notifies it to be updated (i.e. a specific property in that relationship has updated).&lt;/p&gt;
&lt;h2&gt;Creating an RDVP&lt;/h2&gt;
&lt;p&gt;Remember that “Query” tab we talked about earlier that our Virtual Property enabled? That is specifically the editor for RDVPs, and is the secret sauce to making these properties work.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The RDVP Query Configuration Tab&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let’s walk through each of the fields in more detail:&lt;/p&gt;
&lt;h3&gt;Referenced Relationship Fields&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Referenced Relationship Fields&lt;/strong&gt; define &lt;strong&gt;what relationships should be referenced to create this RDVP&lt;/strong&gt;. Since you could be referencing more than one relationship these fields are defined as an array.&lt;/p&gt;
&lt;p&gt;For instance, let’s say that you wanted to traverse the user’s manager relationship. To do so, your “referenced relationship fields” property would be &lt;code&gt;[&quot;manager&quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As a more complicated example, let’s say that you wanted to get a list of data aggregated from all of your managers and reports. Since you are now looking at both your &lt;code&gt;manager&lt;/code&gt; and &lt;code&gt;reports&lt;/code&gt; relationship, your array would look like &lt;code&gt;[[&quot;manager&quot;], [&quot;reports&quot;]]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;More complex still, let’s say that you need to calculate information based on a &lt;strong&gt;relationship’s relationship&lt;/strong&gt;. Perhaps you want an RDVP that gives me a list of all of my coworkers under my manager, i.e. your manager’s reports. If you want to calculate off of a relationship of a relationship, in this case your manager’s reports, your array would look like &lt;code&gt;[[&quot;manager&quot;, &quot;reports&quot;]]&lt;/code&gt;.  If you’re dealing with a singular relationship chain, you don’t need the nested array (we saw that earlier with the manager-only relationship) but for clarity it’s recommended to use the array approach shown here.&lt;/p&gt;
&lt;p&gt;The concepts above can be chained together. Perhaps I want to reference data not only on an nth-level relationship, but also another direct relationship (or another nth-level relationship). If we use the above examples, maybe you want not only data on your manager’s reports (i.e. your coworkers) but also &lt;em&gt;your&lt;/em&gt; reports. Then your array would look like &lt;code&gt;[[&quot;manager&quot;, &quot;reports&quot;], [&quot;reports&quot;]]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can see an example of a complex Referenced Relationship Field in action on the &lt;code&gt;effectiveAssignments&lt;/code&gt; property. This property is looking at both the assignments on the user but also the assignments on the roles related to that user, since roles have their own assignments.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why do the documentation examples not show nested arrays?&lt;/strong&gt; As RDVPs evolved, they went from referencing a single object relationship, to being able to traverse from one relationship to another in the chain, to being able to reference multiple relationships and relationship chains simultaneously. While a single array of values is a valid approach, it doesn’t account for the newer capabilities added to RDVPs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Referenced Object Fields&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Referenced Object Fields&lt;/strong&gt; define &lt;strong&gt;what data is to be gathered from the referenced relationship(s) to create the RDVP.&lt;/strong&gt; This is a comma-separated list of properties that exist on the referenced relationship fields.&lt;/p&gt;
&lt;p&gt;As an example, let’s say you wanted to get a reference to your manager (like what your &lt;code&gt;manager&lt;/code&gt; relationship already does). Your referenced relationship field would be &lt;code&gt;[&quot;manager&quot;]&lt;/code&gt; and the referenced object field can be left blank.&lt;/p&gt;
&lt;p&gt;Now, let’s say that you wanted to get &lt;strong&gt;all&lt;/strong&gt; of the information on your manager. In that case, use &lt;code&gt;*&lt;/code&gt; for your referenced object field.&lt;/p&gt;
&lt;p&gt;It’s likely you don’t need all of that information. Instead, let’s just get the email address of your manager. Your referenced object field would be &lt;code&gt;mail&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now RDVPs get interesting when you start traversing more than one layer in your relationship. What if you wanted the email addresses of &lt;strong&gt;all&lt;/strong&gt; of your managers, i.e. your manager’s manager, your manager’s manager’s manager, and so on?&lt;/p&gt;
&lt;p&gt;Firstly, you could specify a depth of traversal by modifying your referenced relationship field. If you are always getting your manager’s manager’s email address, your Referenced Relationship field would look like &lt;code&gt;[&quot;manager&quot;, &quot;manager&quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since the referenced object field references your relationship’s property, and an RDVP itself is a static property, &lt;strong&gt;you can reference RDVPs as a reference object field in an RDVP&lt;/strong&gt;. So if you wanted to get all of your manager’s emails in the hierarchy and your RDVP is called &lt;code&gt;managerEmails&lt;/code&gt;, your referenced object fields would be &lt;code&gt;mail, managerEmails&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let’s quickly break down how an RDVP referencing itself in its referenced object field works, top down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the above example, the CEO of your company doesn’t have any managers. As a result, both of their referenced object fields would return &lt;code&gt;null&lt;/code&gt; and their &lt;code&gt;managerEmails&lt;/code&gt; property would be &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Let’s say that the CTO reports to the CEO. Their referenced &lt;code&gt;mail&lt;/code&gt; object field would be the &lt;code&gt;mail&lt;/code&gt; of their manager, the CEO. Their referenced &lt;code&gt;managerEmails&lt;/code&gt; field would be &lt;code&gt;null&lt;/code&gt; since the CEO’s is &lt;code&gt;null&lt;/code&gt; and that’s what is being referenced.&lt;/li&gt;
&lt;li&gt;Your boss reports to the CTO. As a result, their referenced &lt;code&gt;mail&lt;/code&gt; object field is the &lt;code&gt;mail&lt;/code&gt; of their manager, the CTO. Their referenced &lt;code&gt;managerEmails&lt;/code&gt; field is the &lt;code&gt;managerEmails&lt;/code&gt; field of the CTO, which contains the CEO’s &lt;code&gt;mail&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Starting to see where this is going? Since you report to your boss, the referenced &lt;code&gt;mail&lt;/code&gt; object field is your boss’ &lt;code&gt;mail&lt;/code&gt;, and your referenced &lt;code&gt;managerEmails&lt;/code&gt; is the &lt;code&gt;managerEmails&lt;/code&gt; field of your boss, which is both the CTO and the CEO’s emails!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these objects are calculated on &lt;strong&gt;store&lt;/strong&gt;, meaning that you have a static field defining hierarchical data that would require many loops or recursions to traditionally aggregate. All of the processing power required to calculate this deeply-nested data is handled once at time of update.&lt;/p&gt;
&lt;p&gt;A useful callout: If you are referencing more than one relationship field, only the relationship(s) with the referenced object field will provide data. As an example, if your referenced relationship fields looked like &lt;code&gt;[[&quot;manager&quot;], [&quot;adminOfOrg&quot;]]&lt;/code&gt; and your referenced object fields looked like &lt;code&gt;mail&lt;/code&gt;, unless you added an email property to your Organization you will only be getting the email of your manager.&lt;/p&gt;
&lt;h3&gt;Flatten Properties&lt;/h3&gt;
&lt;p&gt;RDVPs will return by default an array of objects which includes the referenced object fields you’ve requested, the &lt;code&gt;_id&lt;/code&gt; of the managed object’s &lt;strong&gt;relationship&lt;/strong&gt; (i.e. the relationship ID), and the &lt;code&gt;_rev&lt;/code&gt; (i.e. the revision of that relationship). Flatten properties will return an array containing just the requested fields. Even if you request more than one field, those fields will be concatenated together in the array &lt;strong&gt;in sequential order of listing&lt;/strong&gt;. This makes managing, and interacting with, multiple object fields much easier.&lt;/p&gt;
&lt;p&gt;Let’s take the above example. If you set up a referenced relationship field of &lt;code&gt;[&quot;manager&quot;]&lt;/code&gt; and a referenced object field of &lt;code&gt;mail,managerEmails&lt;/code&gt; by default your response may look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;managerEmails&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;mail&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;theBoss@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;managerEmails&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &quot;mail&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;theCTO@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &quot;managerEmails&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &quot;mail&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;theCEO@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &quot;managerEmails&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &quot;_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;7162ddd4-591a-413e-a30b-3a5864bee5ec&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        &quot;_rev&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &quot;_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;02b166cc-d7ed-46b7-813f-5ed103145e76&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                &quot;_rev&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;f1e2d3c4-b5a6-7890-1234-56789abcdef0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;_rev&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yuck. That’ll get annoying to traverse real quickly.&lt;/p&gt;
&lt;p&gt;With flatten properties enabled, your result looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;managerEmails&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;theBoss@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;theCTO@example.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;theCEO@example.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Much nicer.&lt;/p&gt;
&lt;p&gt;When flattening properties, it’s important to understand what data types you are flattening. It’s best practice to &lt;strong&gt;flatten only if you are working with string, boolean, or number properties and if all of your properties are of the same type&lt;/strong&gt;. Otherwise, you’ll end up with a jumble of objects in an array that will be very difficult to parse later.&lt;/p&gt;
&lt;h3&gt;Delete Property Configuration&lt;/h3&gt;
&lt;p&gt;This is a UI-only addition that will remove the above configuration. You can accomplish the same by deleting the values in their respective fields (which is probably best considering the UI keeps these values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Deleting your property configuration doesn’t delete the data on your property. Just because you changed a configuration doesn’t change the fact that the data only updates when it’s notified. If you are greatly overhauling what an RDVP is meant to do it may be wise to create a new property or define a means to notify all of the objects that contain that property to update.&lt;/p&gt;
&lt;h2&gt;Notifications&lt;/h2&gt;
&lt;p&gt;For an RDVP to be calculated at all, there needs to be a &lt;a href=&quot;https://docs.pingidentity.com/pingidm/7.5/objects-guide/relationships-notification.html&quot;&gt;relationship change notification&lt;/a&gt;. A relationship change notification indicates to related objects that “hey, something has happened here that you care about” which informs those objects to update their RDVPs accordingly.&lt;/p&gt;
&lt;p&gt;There are three ways in which a notification can be configured on a property:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Notify Relationships&lt;/strong&gt;, under the advanced details of a property&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify&lt;/strong&gt;, under the relationship editor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify Self&lt;/strong&gt;, under the advanced details of a relationship&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The “Notify” and “Notify Self” flags signal to the base objects when &lt;strong&gt;the relationship has changed&lt;/strong&gt; (e.g. a relationship has been added or removed). Notify will notify the downstream relationship, and Notify Self will notify the object you’re currently on.&lt;/p&gt;
&lt;p&gt;The “Notify Relationships” flag signals to the specified relationship when &lt;strong&gt;the specific property has changed&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Rarely you may need to notify a property to update that isn’t directly related to you. To do so, &lt;strong&gt;use “Notify Relationships” on a relationship property to notify indirect relationships&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Notifications Example&lt;/h3&gt;
&lt;p&gt;Looking back at our manager emails example:&lt;/p&gt;
&lt;p&gt;The manager emails property is meant to store a record of the email addresses of a user’s managers up the management hierarchy. That means that the value should recalculate if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The employee gets a new manager&lt;/li&gt;
&lt;li&gt;Any manager upstream (e.g. the manager’s manager) has been changed&lt;/li&gt;
&lt;li&gt;Any manager upstream has changed their email address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In terms of notifications, then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The owner of the &lt;code&gt;manager&lt;/code&gt; relationship should &lt;strong&gt;Notify Self&lt;/strong&gt; which tells itself to update when the relationship has changed.&lt;/li&gt;
&lt;li&gt;The owner of the &lt;code&gt;reports&lt;/code&gt; relationship should &lt;strong&gt;Notify&lt;/strong&gt; which tells the reports to recalculate based on this relationship changing.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;mail&lt;/code&gt; attribute should &lt;strong&gt;Notify Relationships&lt;/strong&gt; to the &lt;code&gt;reports&lt;/code&gt; that the email has changed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common trip-up when defining notifications is that your relationship definition describes what it’s pointing to, not what it lives on. Your manager has reports: you have a manager. If you need to update yourself when you change your manager, you need to put &lt;strong&gt;Notify Self&lt;/strong&gt; on your Manager relationship property and not on your reports property. If your manager needs to let you know that something has changed, they need to &lt;strong&gt;Notify&lt;/strong&gt; their reports property.&lt;/p&gt;
&lt;p&gt;Let’s look at a more complicated real-world example: &lt;code&gt;effectiveAssignments&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Effective Assignments lists &lt;strong&gt;all assignments tied to a user&lt;/strong&gt;. Assignments can be directly assigned to a user (via the &lt;code&gt;assignments&lt;/code&gt; relationship) but they can also be assigned to a role which then is assigned to a user (via the &lt;code&gt;roles&lt;/code&gt; relationship). As a result, effective assignments change if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The user has added or removed an assignment relationship&lt;/li&gt;
&lt;li&gt;The user has added or removed a role relationship&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;role related to a user&lt;/strong&gt; has added or removed an assignment relationship&lt;/li&gt;
&lt;li&gt;Any of the related assignments have changed their properties&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start by taking a look at the Query Configuration for &lt;code&gt;effectiveAssignments&lt;/code&gt; on the user. You’ll see that it’s referencing both the assignments directly related to the user as well as the assignments tied to roles (&lt;code&gt;[[&quot;roles&quot;,&quot;assignments&quot;],[&quot;assignments&quot;]]&lt;/code&gt;), and that it’s returning all the properties within the assignments (&lt;code&gt;*&lt;/code&gt;). This definition should inform you that notifications will be occurring on the &lt;code&gt;user&lt;/code&gt; (where this property is located), &lt;code&gt;roles&lt;/code&gt;, and &lt;code&gt;assignments&lt;/code&gt; and that we need to notify based on properties updating from &lt;code&gt;assignments&lt;/code&gt; specifically.&lt;/p&gt;
&lt;p&gt;Looking at these relationships, you’ll see the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the Assignment object:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Notify&lt;/strong&gt; is enabled for its &lt;code&gt;members&lt;/code&gt; relationship. This notifies the &lt;strong&gt;user&lt;/strong&gt; linked to the assignment if their relationship has changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify&lt;/strong&gt; is enabled if its &lt;code&gt;roles&lt;/code&gt; relationship. This notifies the &lt;strong&gt;role&lt;/strong&gt; linked to the assignment if their relationship has changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify Relationships&lt;/strong&gt; is enabled for &lt;code&gt;roles&lt;/code&gt; and &lt;code&gt;members&lt;/code&gt; on the &lt;code&gt;attributes&lt;/code&gt; property and the &lt;code&gt;weight&lt;/code&gt; property. This notifies &lt;strong&gt;users and roles&lt;/strong&gt; if the attributes and weight properties on the assignment have changed, as these properties are referenced in RDVPs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On the Role object:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Notify&lt;/strong&gt; is enabled for its &lt;code&gt;members&lt;/code&gt; relationship. This notifies the &lt;strong&gt;user&lt;/strong&gt; linked to the role if their relationship has changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify Self&lt;/strong&gt; is enabled on the &lt;code&gt;assignments&lt;/code&gt; relationship. This notifies the &lt;strong&gt;role&lt;/strong&gt; if the relationship has changed with the assignment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify Relationships&lt;/strong&gt; is enabled for &lt;code&gt;members&lt;/code&gt; on the &lt;code&gt;assignments&lt;/code&gt; &lt;strong&gt;relationship&lt;/strong&gt;. This notifies the &lt;strong&gt;user&lt;/strong&gt; if the &lt;strong&gt;role’s assignments&lt;/strong&gt; have changed, which is used in the effectiveAssignments RDVP definition.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On the User object:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Notify Self&lt;/strong&gt; is enabled on both the &lt;code&gt;assignments&lt;/code&gt; and &lt;code&gt;roles&lt;/code&gt; relationships. This notifies the &lt;strong&gt;user&lt;/strong&gt; if their relationships have changed with either assignments or roles.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Enough talk: let’s build an RDVP and see it in action.&lt;/p&gt;
&lt;h2&gt;RDVP Examples&lt;/h2&gt;
&lt;p&gt;An RDVP requires a relationship to derive the property. So that we don’t alter notifications for pre-existing relationships, let’s create a brand new relationship that we can use.&lt;/p&gt;
&lt;p&gt;This example will expand on the manager and employee concept and add in &lt;strong&gt;departments&lt;/strong&gt;. Departments are going to link together users and other departments so that we can create a hierarchical view of our business.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Departmental Structure&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To start, head back to the Native IDM Console. Under the Managed Objects view (Configure → Managed Objects) click the New Managed Object button and give it the following details:&lt;/p&gt;
&lt;div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed Object Name&lt;/td&gt;
&lt;td&gt;department&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readable Title&lt;/td&gt;
&lt;td&gt;Department&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;The rest of the values can be left blank. Click Save, and then using the “Add a Property” button on the next screen add a property with the name &lt;code&gt;name&lt;/code&gt;, the Label &lt;code&gt;Name&lt;/code&gt;, the type &lt;code&gt;String&lt;/code&gt;, and make the value required. In the advanced properties, set this value to “Searchable”.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Base Department Object&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Go to your Identity Management Screen (in AIC, that’s in the Platform UI under Identities → Manage, and in PingIDM that’s the Manage Tab) and click on the Departments object. You should be able to create a new Department with its own name.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;An Example Department&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, let’s add the relationships to our Department object.&lt;/p&gt;
&lt;p&gt;The Department should be able to have &lt;strong&gt;employees&lt;/strong&gt; and should be able to have &lt;strong&gt;subdepartments&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the department management page, add a new property called &lt;code&gt;employees&lt;/code&gt; with the type &lt;code&gt;relationship&lt;/code&gt; and hit save. You’ll be taken to the relationship management page - here you should select your User (in AIC, that’s &lt;code&gt;alpha_user&lt;/code&gt; for the alpha realm) and select the &lt;code&gt;userName&lt;/code&gt; and &lt;code&gt;mail&lt;/code&gt; properties as display properties. Hit save.&lt;/p&gt;
&lt;p&gt;You’ve now created a unidirectional linkage between a department and a user, in which that linkage will display the username and the email address of that employee in REST calls and the Platform UI.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Employees Relationship Resource Linkage&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The center of the relationship property uses arrows to define how the relationship links one property to another. Right now you should see a connection from &lt;strong&gt;department to employee&lt;/strong&gt; with a bubble in the middle that says &lt;code&gt;“Has one employees”&lt;/code&gt;. A department can have more than one employee, so click that bubble and change the relationship from “one” to “many”. Hit Save.&lt;/p&gt;
&lt;p&gt;Currently your relationship is &lt;strong&gt;unidirectional&lt;/strong&gt;: only the department object is aware of the linkage to the user. It’s likely that you need to be able to reference this relationship from the user as well, just as it’s likely that the relationship should be assignable from the user. Select the “Two-way Relationship” bubble that’s over the arrow pointing from your user object to your department object and select the “Has one” option. Create a reverse property with the name &lt;code&gt;custom_department&lt;/code&gt;, giving it the display property of &lt;code&gt;name&lt;/code&gt;. Hit save, and then save again on the property page.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Employee/Department Relationship&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You’ll probably notice that your reverse relationship will by default set the Readable Title to the name of the relationship property. To change, go to the relationship definition and update the property directly.&lt;/p&gt;
&lt;p&gt;We are going to create one more relationship: the concept of a parent department and children departments. This will give us the nesting that we outlined at the beginning of this example.&lt;/p&gt;
&lt;p&gt;Create a new relationship entitled &lt;code&gt;parent&lt;/code&gt; with the resource of &lt;code&gt;department&lt;/code&gt; and the display property of &lt;code&gt;name&lt;/code&gt;. Make it a bidirectional relationship, has many, with the reverse property name of &lt;code&gt;children&lt;/code&gt; and the same display property as the other direction (&lt;code&gt;name&lt;/code&gt;). This new relationship indicates that a department can have many children but only one parent.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Parent/Children Relationship&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Go back to the platform UI where you defined your Example organization. You’ll see two new tabs - one that lets you set Employees and another where you can set Children, as well as a dropdown that lets you select another Department as the Parent of the Department you’re on.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Updated Department Object, with Relationships&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Alright, we’ve created our example relationship with some bidirectional linkages that branch across more than one object. Let’s create some RDVPs to make referencing nested data easier.&lt;/p&gt;
&lt;h3&gt;Example: Single-Level Linkage&lt;/h3&gt;
&lt;p&gt;A department can be a parent of one or more departments. As an administrator, be it HR, management, or otherwise, you likely want to quickly understand what departments are children of your department.&lt;/p&gt;
&lt;p&gt;Let’s create an RDVP that lists the names of all of the children departments of the parent.&lt;/p&gt;
&lt;p&gt;Inside the department object, create an Array property with the name of &lt;code&gt;childNames&lt;/code&gt; and label of &lt;code&gt;Names of Children&lt;/code&gt;. Click into that property, unset User Editable, and set Virtual and Return By Default (we need that still for the Platform UI). Next, go to the Query Configuration and set the referenced relationship field as &lt;code&gt;[&quot;children&quot;]&lt;/code&gt; and the referenced object field as &lt;code&gt;name&lt;/code&gt;. Hit save.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The ChildNames Property&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create a brand-new department (I’m naming mine &lt;code&gt;Child1&lt;/code&gt;) and assign your &lt;code&gt;Example&lt;/code&gt; department as a parent. Go back to your Example department and…&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Something Seems Off&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;…wait, where’s the Child Name?&lt;/p&gt;
&lt;p&gt;While the RDVP’s query is configured to get the data we need, we haven’t configured any way to &lt;strong&gt;notify&lt;/strong&gt; the RDVP to update.&lt;/p&gt;
&lt;p&gt;The department will need to be notified in the following locations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the &lt;strong&gt;parent&lt;/strong&gt; has added/removed a &lt;strong&gt;child&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;When the &lt;strong&gt;child&lt;/strong&gt; has assigned/unassigned a &lt;strong&gt;parent&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;First, in the parent relationship enable &lt;strong&gt;Notify&lt;/strong&gt;. This tells the children to notify their parent to update when their relationship has changed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning: Never set Notify on a non-reverse relationship. This will corrupt your managed JSON.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Notify on the Parent Department Relationship&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, in the children relationship enable &lt;strong&gt;Notify Self&lt;/strong&gt;. This tells the parent that it should update when the parent/child relationship has been changed, which it needs to update the Child Names.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Notify Self on the Children Department Relationship&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Head back to your Example department. You’ll now see that… the child names still aren’t there?&lt;/p&gt;
&lt;p&gt;Remember, &lt;strong&gt;RDVPs update only when they are notified to update&lt;/strong&gt;. Since that child relationship was set before we created our notifications, nothing informed the RDVP to update.&lt;/p&gt;
&lt;p&gt;In your Example department, remove Child1 from your Children and then re-add them. Go back to your details page (you may need to refresh the page afterwards, too, to see the change) and you’ll see an odd jumble of JSON.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Raw RDVP&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Raw JSON will give you a better sense of what’s going on here. Your RDVP is returning the relationship details along with the value you’re requesting from the object (i.e. the name).&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The RDVP JSON Output&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In some cases, this level of detail is exactly what we want - it gives us the extra information we need to perform additional analysis like relationship querying. In the case of our name list, though, it’s probably unnecessary.&lt;/p&gt;
&lt;p&gt;Head back to your Query Configuration for the &lt;code&gt;childNames&lt;/code&gt; property and check “Flatten Properties”, then unassign and reassign the child relationship (if you want, try to assign the parent from Child1 to show that your notifications are working from both sides of the relationship). Your results are now returning as an array of just the child names.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Much Better&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But what if you change the name of the child? Right now, nothing would happen - your parent is only getting notified if the &lt;strong&gt;relationship&lt;/strong&gt; changes, not if the &lt;strong&gt;property&lt;/strong&gt; changes.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;name&lt;/code&gt; property of your department object, add the &lt;code&gt;parent&lt;/code&gt; relationship to &lt;strong&gt;Notify Relationships&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Notifying the Parent if the Name Changes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;With that notification in place, if your department changes their name they immediately let their parent know to update their RDVP accordingly.&lt;/p&gt;
&lt;p&gt;You now have an RDVP that gives you an easy glance into the direct descendants of your department. But what if your sub-departments can go a level deeper?&lt;/p&gt;
&lt;h3&gt;Example: Multi-Level Linkage&lt;/h3&gt;
&lt;p&gt;Create a new department named &lt;code&gt;Grandchild1&lt;/code&gt; and assign its parent to &lt;code&gt;Child1&lt;/code&gt;. You’ll see that the Child can see the Grandchild but the Parent (the Example department) has no visibility into the grandchild. If you’ve created a property that is meant to give visibility into all of the sub-departments that your department oversees, it’s likely that you want to see &lt;strong&gt;all&lt;/strong&gt; of them, not just the first layer of direct assignments.&lt;/p&gt;
&lt;p&gt;Back in your &lt;code&gt;childNames&lt;/code&gt; RDVP, update the referenced object fields to be &lt;code&gt;name,childNames&lt;/code&gt;. This tells the RDVP to not only reference the name of your children, but the childNames property on your children.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Nested Referenced Object Fields&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unassign and reassign the grandchild from the child. The parent (example) department should have the grandchild, right?&lt;/p&gt;
&lt;p&gt;Nope! Right now we are letting our direct relationships know when changes have been made but we aren’t propagating that information farther along the hierarchy. In other words: our parents know when they’ve changed children, and our children are letting our parents know when they’ve changed their parents, but our children aren’t letting our parents know when they’ve changed their children.&lt;/p&gt;
&lt;p&gt;Back in the &lt;code&gt;children&lt;/code&gt; property, set &lt;strong&gt;Notify Relationships&lt;/strong&gt; to &lt;code&gt;parent&lt;/code&gt;. This says that when you’ve modified your children you will let your parent know that something has changed and that they need to update their own information accordingly.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;Notifying the Parent from the Children Relationship&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From Child1, unassign and reassign Grandchild1 again. Going back to Example, you’ll see both the child and grandchild in your RDVP.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Multi-Level RDVP&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now we have a way to see nested relationship data in a handy-dandy, easy-to-reference array. Let’s take this a step further and get data from another relationship in our department: the &lt;strong&gt;employees&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Example: Multi-Relationship Linkage&lt;/h3&gt;
&lt;p&gt;If you’re getting the names of the departments you cover, you may want to also get some details on the employees that work across those departments.&lt;/p&gt;
&lt;p&gt;Create a new Array property called &lt;code&gt;employeeEmails&lt;/code&gt; with the label &lt;code&gt;Emails of Employees&lt;/code&gt;. Like before, disable “User Editable” and enable both “Virtual” and “Return by Default”.&lt;/p&gt;
&lt;p&gt;Your query configuration will now have to reference two different relationships: your department’s &lt;strong&gt;employees&lt;/strong&gt; as well as their &lt;strong&gt;children&lt;/strong&gt;. The employees relationship will give us the data for the employees that work directly for this department and the children will give us information about the employees that work in the child departments.&lt;/p&gt;
&lt;p&gt;Set your referenced relationship fields to &lt;code&gt;[[&quot;employees&quot;],[&quot;children&quot;]]&lt;/code&gt;, your referenced object fields to &lt;code&gt;mail,employeeEmails&lt;/code&gt;, and enable Flatten Properties. This query is stating that we are looking for the mail and employeeEmails properties on both the employees relationship and the children relationship. Since mail only exists on employees and employeeEmails only exist on children we’ll get a running list of email addresses across the direct and indirect assignments.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Employee Emails Query Configuration&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Taking what we’ve learned from the prior examples, we should know that while the children of the department are notifying the department when changes are made, we’ll need to additionally notify the department about the employees to keep this RDVP up to date. If you want to test your understanding, try to set up the notifications yourself before checking your work below.&lt;/p&gt;

  Solution (Click to View)
&lt;ul&gt;
&lt;li&gt;In the department managed object:
&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;Notify Self&lt;/strong&gt; to &lt;strong&gt;true&lt;/strong&gt; on the &lt;code&gt;employees&lt;/code&gt; relationship&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Notify Relationships&lt;/strong&gt; to &lt;code&gt;parent&lt;/code&gt; on the &lt;code&gt;employees&lt;/code&gt; relationship&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the user managed object:
&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;Notify&lt;/strong&gt; to &lt;strong&gt;true&lt;/strong&gt; on the &lt;code&gt;custom_departments&lt;/code&gt; relationship&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Notify Relationships&lt;/strong&gt; to &lt;code&gt;custom_departments&lt;/code&gt; on the &lt;code&gt;mail&lt;/code&gt; property&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create three users: &lt;code&gt;parentEmployee&lt;/code&gt;, &lt;code&gt;childEmployee&lt;/code&gt;, and &lt;code&gt;grandchildEmployee&lt;/code&gt;. Then, either via the admin interface for the user or the department, assign the users accordingly. You should see their emails pass upward to your parent department.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;
&lt;em&gt;The Multi-Relationship RDVP in Action&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Example: The Organization Model&lt;/h3&gt;
&lt;p&gt;You may have noticed something familiar about the model we created. In essence, the hierarchy we built and the RDVPs we designed are a slightly-modified, rudimentary implementation of the Organization Model in AIC. On top of the RDVPs we built, Organizations have two additional User/Organization relationships with their own RDVPs gathering those relationships to be used in the platform for things like &lt;a href=&quot;https://gwizkid.com/posts/setting-delegated-admin-privileges-in-aic/&quot;&gt;delegated administration&lt;/a&gt;. Relationships, and RDVPs, are the foundational principles that the Organization structure is built upon - and you now know how it works and how to expand upon it!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By completing this Guide, you’ve learned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are Virtual Properties and Relationship-Derived Virtual Properties&lt;/li&gt;
&lt;li&gt;How to choose between the two types&lt;/li&gt;
&lt;li&gt;Once chosen, how to configure the property&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Outside of just Virtual Properties, you’ve additionally learned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Common IDM scripting techniques&lt;/li&gt;
&lt;li&gt;The basics of Relationships and a detailed understanding of configuring notifications from those relationships&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Armed with this knowledge, you should be able to design and build complex, dynamic properties that match the requirements you need for your business and applications.&lt;/p&gt;
&lt;p&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.C8Eq5Sfq_1n4otN.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Hardening the handoff: a pattern for app-to-web single sign-on</title><link>https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on/</guid><description>A walkthrough of a standards-based pattern for securely transferring an OAuth session from a native mobile app to a web browser using Pushed Authorization Requests (PAR).</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@robertotto_31404/hardening-the-handoff-a-pattern-for-app-to-web-single-sign-on-6caf0c82786b&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We’ve all been there, haven’t we? That shiny mobile app that you’ve just downloaded from your favourite brand, the one that’s offered you a really clean and modern sign in experience (without browser hand-offs, perhaps even using a passkey for quick login), suddenly and inexplicably spits you out into a browser view when you tap a particular button or menu item. Hybrid apps — those that combine both native and web elements — are an unavoidable reality for many organisations (and their users) as they look to balance time to market on new features with ever-stretched engineering budgets and the technical debt further down the stack that they’ve never quite managed to pay off.&lt;/p&gt;
&lt;p&gt;As identity professionals who care about that delicate balance between user experience and security, the question that comes up time and again is this: what is the best way to secure these apps? How do I ensure a slick user authentication process at the start and then safely re-use that session across all aspects of my application, when some parts of it are delivered via browser or web-view? In this article, I’ll explore the challenges that these hybrid app models present to us and propose a workable solution based on modern identity standards (with a sprinkling of identity orchestration). The solution I describe below has been built and tested using &lt;strong&gt;Ping Identity’s &lt;a href=&quot;https://docs.pingidentity.com/pingone/getting_started_with_pingone/p1_p1sso_start.html&quot;&gt;PingOne SSO&lt;/a&gt;&lt;/strong&gt; identity provider together with the &lt;a href=&quot;https://docs.pingidentity.com/davinci/davinci_landing_page.html&quot;&gt;&lt;strong&gt;PingOne DaVinci&lt;/strong&gt; identity orchestration engine&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Eager for the punchline? &lt;a href=&quot;#tldr&quot;&gt;Here’s the succinct summary&lt;/a&gt; for those on a time and attention budget!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The session transfer problem&lt;/h2&gt;
&lt;p&gt;Before we can start to solve the challenges that we face in securing hybrid mobile applications, we should start by better defining the problem space. For the purposes of this discussion, we define a &lt;strong&gt;hybrid mobile application&lt;/strong&gt; as a native application running on a mobile device where certain portions of the application’s functionality are delivered via a web browser or web view on the device — that is to say, the app is not fully native. The exact specifics of how and when the app switches context between a native delivery versus a browser-based delivery of functionality are not important, however there are two requirements that we must fulfil, namely&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;access to the browser-based content requires an authenticated user session (presumably in the form of a browser session cookie, which is how these things have always been done)&lt;/li&gt;
&lt;li&gt;the user has already performed authentication in order to access the app itself and as such should not be prompted to re-authenticate in order to access the browser-based content.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A number of follow-on points are salient here and those who have grappled with this situation in the past are no doubt familiar with them. The most important ones are, perhaps, as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In a native application context, the authenticated user session is typically represented by an OAuth access token, issued to the application by a trusted Authorisation Server in response to a user login.&lt;/li&gt;
&lt;li&gt;There are &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8252&quot;&gt;established best practices&lt;/a&gt; for how a native application should go about obtaining such a token by leveraging the Auth Code Flow with PKCE via the system browser. This approach is battle-hardened and peer-reviewed and should always be the first alternative considered when building a user authentication process for a native app.&lt;/li&gt;
&lt;li&gt;To directly contradict the previous point, however, many (if not most) digital product owners and UX designers find the experience trade-off unacceptable and thus strongly resist using a browser-delivered authentication ceremony within the context of a native application, preferring to use fully native SDKs to interact directly with the authentication service to obtain the necessary access token.&lt;/li&gt;
&lt;li&gt;Whichever approach is selected for initial user authentication, the basic requirement is the same in terms of providing a single-sign-on experience if the app includes hybrid web content; in order for the user to access said content without re-authentication, there needs to be a session cookie present in the browser at the time that content is requested. This is the most basic requirement (the &lt;em&gt;sine qua non&lt;/em&gt;, if you will) without which the user is guaranteed to be prompted for repeat authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this talk of cookies is possibly making you hungry by now, but we do need to explore the intricacies just a little deeper before taking a well-earned coffee break! This problem really is all about cookies, though — that much we cannot deny. One would think and would hope that the pattern of using the system browser to obtain authentication credentials from the user in the first place would create a session cookie in that browser as a serendipitous side effect (and give us a further piece of ammunition in convincing our product owner and UX colleagues to eschew a fully-native alternative). In practice, however, this approach has been shown to be non-deterministic at best, if not downright flaky, due to the somewhat arcane and seemingly ever-changing rules regarding cookie persistence and visibility across browser tabs and invocations. Without going into all of the details, I think it is sufficient to say that, based on the experiences of many doing this work in the field today, we cannot assume that a session cookie set during initial login will be available during subsequent browser-based interactions. The reasons for this are all very well-meant; examples being Apple’s &lt;a href=&quot;https://www.avenga.com/magazine/intelligent-tracking-prevention/&quot;&gt;Intelligent Tracking Prevention&lt;/a&gt; feature and Google’s equivalent Privacy Sandbox work — designed to increase user privacy. As a result, even if we are lucky enough to stumble upon a configuration on a given device that does give us the cookie visibility that we need, there is every chance that further tightening of cookie rules in the future could break our solution, leaving both product owners and (more importantly) end users furious.&lt;/p&gt;
&lt;p&gt;What we are left with is a challenge to solve, broadly defined as follows:&lt;/p&gt;
&lt;p&gt;“how, then, do we engineer a mechanism where the access token that represents the logged-in user within the native context can be used to create a browser session cookie at the point that the user needs to access web content”&lt;/p&gt;
&lt;p&gt;It goes without saying that any such mechanism MUST be secure and avoid exposing us and our users to malicious theft of tokens, user sessions and the applications and API’s that these protect.&lt;/p&gt;
&lt;h2&gt;A standards-based pattern for secure session transfer&lt;/h2&gt;
&lt;p&gt;We know that we need a mechanism whereby we can take the app’s representation of an authenticated user session, the OAuth access token, and turn this into an authenticated session in a web browser, in the form of a session cookie. OAuth does not provide a standardised pattern or grant type that we can use to solve this problem and as a result a number of approaches have emerged, many of which fall down on either user experience or security, and sometimes both.&lt;/p&gt;
&lt;p&gt;A naive approach to the problem would be to launch a new OAuth authorisation request in the browser via the Auth Code flow, and simply pass the app’s access token directly to the Authorisation Server as a custom URL parameter. This does imply that whatever OAuth Server implementation is in use must provide the capability to extract the access token from the query string, validate it and create a web session cookie for the subject of the token. While not many implementations that I’ve encountered will provide this sort of “access-token-to-web-session” mechanism out of the box, most will offer some sort of customisation capability, via plug-in or extension script that will allow such a mechanism to be built.&lt;/p&gt;
&lt;p&gt;I would advise against this approach for a number of reasons, with the most critical being the security risk that it carries.&lt;/p&gt;
&lt;p&gt;We must remember, at all times, the inherent security challenge implied by the Bearer Token model, where simple possession of an access token grants whoever holds the token full power to use it. We should assume that any access token issued to a native application client would be a powerful artefact indeed and allow a malicious actor the ability to invoke any API on the application back-end should they manage to obtain it. &lt;strong&gt;Adding such a dangerous object to a URL query string and sending it out “into the wild” where it could almost trivially be intercepted is not the solution we seek.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The alternative approach that I recommend is to use a relatively-recent addition to the OAuth family of standards to achieve the same outcome in a way that provides better protection of the access token during the session transfer process, namely OAuth 2.0 Pushed Authorization Requests (or PAR), described in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9126&quot;&gt;RFC 9126&lt;/a&gt;. To set the scene, let’s look at the abstract of the document that describes the approach:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“This document defines the pushed authorization request (PAR) endpoint, which allows clients to push the payload of an OAuth 2.0 authorization request to the authorization server via a direct request and provides them with a request URI that is used as reference to the data in a subsequent call to the authorization endpoint.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To paraphrase and simplify, PAR allows us to provide our sensitive access token directly to the Authorisation Server via a direct HTTPS Post from our native app code, rather than including it as an insecure query string parameter in the browser. This separation of data payloads between the “back channel” (direct POST) and the “front channel” (browser GET) significantly improves the security baseline of what we need to achieve and provides solid scaffolding for the further work we need to do.&lt;/p&gt;
&lt;p&gt;An additional advantage to the approach is that PAR gives us a ready-made query string parameter, in the form of the returned &lt;em&gt;request_uri&lt;/em&gt;, that is guaranteed to be single-use and short-lived, thus reducing the very large attack surface that the alternative approach inherently creates.&lt;/p&gt;
&lt;p&gt;I mentioned further work above, and it is important to realise that while PAR does provide a great starting point, it is not the complete solution to this challenge by itself. There is more to be done both in terms of user experience and also security, which we’ll explore in the next section. For now, though, let’s describe the “bones” of the approach here, in the form of a worked example&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Alice opens the Acme Inc native application and clicks the “Login” button. She is authenticated via a native authentication experience (no browser pop-up) that results in a valid OAuth access token being sent to the app. The app responsibly and securely stores this access token on her behalf.&lt;/li&gt;
&lt;li&gt;Alice clicks the “Manage My Profile” button in the app. Acme Inc delivers profile management via an embedded browser view within the app, and the web backend that serves this content requires a session cookie in order to identify Alice and link her to the correct profile.&lt;/li&gt;
&lt;li&gt;When Alice clicks the button, native app code retrieves her access token and initiates a secure POST to the Acme Inc Authorisation Server’s PAR Endpoint. The post body includes access token as a custom parameter, in addition to client_id, scope and all the other required OAuth parameters. In accordance with the PAR standard, the Authorisation server returns a response including a &lt;em&gt;request_uri&lt;/em&gt;, a single use value that acts as a reference to the request data that has been provided.&lt;/li&gt;
&lt;li&gt;The native app code now opens a browser window, passing in the URL of the Acme Inc Authorisation Server’s authentication endpoint, with the &lt;em&gt;request_uri&lt;/em&gt; from the previous step as a parameter. The Authorisation Server uses identity orchestration to validate the provided access token and create a web session for Alice, before redirecting her to the profile management application, where the browser cookie now ensures a single-sign-on experience&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The informed reader will, of course, have many remaining questions based on the above explanation. Let’s address them now.&lt;/p&gt;
&lt;h2&gt;Enabling secure session transfer on top of PAR&lt;/h2&gt;
&lt;p&gt;Native applications are public clients from an OAuth point of view. Put another way using language extracted directly from &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot;&gt;RFC6749&lt;/a&gt;, &lt;em&gt;“it is assumed that any client authentication credentials included in the application can be extracted”&lt;/em&gt;. The implications of this reality are far reaching and many of them beyond the scope of this discussion, but it matters here because it means we cannot use a confidential client (with a client secret) to kick off our PAR request from inside the app.&lt;/p&gt;
&lt;p&gt;It is important to realise here that we are not using PAR for its ability to complete an end-to-end flow, that ends with a redirection and ultimate token issuance to the same OAuth client that initiated the request. We are rather using PAR as a way to initiate a browser flow and then build on the side effect of the session cookie that is created. This means that we do not, in fact, want to ever complete the flow that we start via PAR. We only want to go as far as creating a session cookie before we break out and redirect the user to the actual application, which will start its own Auth Code flow from scratch, using its own client_id, redirect_uri, PKCE enforcement, etc.&lt;/p&gt;
&lt;p&gt;The combination of the two factors above suggests that we should configure and use a specific OAuth Client as a sort of session transfer proxy — that is, a completely new client unrelated to either the native app that starts the transfer or the web app that ultimately benefits. We should take as many precautions as we can to lock down this session transfer client such that it can only be used for a single purpose, but also we should be sure not to add any configurations to our existing clients that could weaken their existing security posture.&lt;/p&gt;
&lt;p&gt;Let’s describe the flow again, adding the additional details that we have described above.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Alice clicks the “Manage my profile” button in the app. The app code starts the PAR process using the client_id of the dedicated Session Transfer Client. It includes a scope such as “session_transfer” in the request, and generates a PKCE Code Challenge that it includes with the request. Note that the corresponding Code Verifier can be discarded at this point since this flow is not intended to ever result in an access token issuance.&lt;/li&gt;
&lt;li&gt;The app code includes three other pieces of information (at minimum). These are the access token that it already holds for the user, the IP address that it is using, and the URL of the profile management application.&lt;/li&gt;
&lt;li&gt;It sends the PAR request to the authorisation server and obtains a response that contains a request_uri, which it appends to the AS’s authorization endpoint URL before opening a browser tab to initiate the front-channel flow.&lt;/li&gt;
&lt;li&gt;When the front-channel request hits the Authorisation Server, the request is validated (to ensure it has not expired or already been used) before being handed to the identity orchestration engine for further processing.&lt;/li&gt;
&lt;li&gt;The orchestration engine (as above, I have tested this with PingOne DaVinci, mileage may vary if using a different platform) must introspect or validate the provided access token to ensure that it is valid, has not expired and was indeed issued to the expected client (the mobile app). It should also compare the IP address provided as part of the PAR request to the IP address passed via the current HTTP request, to ensure that the backend and frontend requests come from the same place. Further checks can be implemented using orchestration logic as required.&lt;/li&gt;
&lt;li&gt;Should the above checks succeed, the orchestration engine extracts the “sub” claim from the access token to determine the appropriate user, and sets a session cookie in the browser for that same user.&lt;/li&gt;
&lt;li&gt;At this point, the orchestration logic simply redirects the browser to the URL of the profile management application (the same URL that was provided via the PAR back-channel initiation in step 3). This is the step that interrupts the flow started by the Session Transfer Client and ensure that no auth code or access token is ever issued.&lt;/li&gt;
&lt;li&gt;The profile management application now receives the browser redirect and is responsible for initiating its own OAuth/OpenID Connect Auth Code flow, just as it would had a browser opened the app URL directly. It should generate its own redirect to the AS, including its own client_id and redirect_uri, its own appropriate scopes and its own PKCE Code Challenge. The AS will start a user authentication process that should complete with no further user interaction required, based on the presence of the session cookie set in step 6.&lt;/li&gt;
&lt;li&gt;The browser is thus redirected back to the profile management application with everything that is needed to display logged-in user content.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;My good friend and proof-reader &lt;a href=&quot;https://medium.com/@gransomeuk&quot;&gt;Gransomeuk&lt;/a&gt; pointed out that some folks will just want the take away, rather than the full three course meal. With his help, here’s the summary, with a nifty sequence diagram to match.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 (Back-Channel):&lt;/strong&gt; The mobile app passes the highly sensitive access token directly to the Authorization Server over a secure connection (POST). In return, it gets a short-lived reference string (&lt;code&gt;request_uri&lt;/code&gt;). This solves the problem of putting the Access Token in a visible URL query string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2 (Front-Channel):&lt;/strong&gt; The app opens the browser using only the reference string. The Identity Orchestration engine (e.g., PingOne DaVinci) verifies that the person in the browser is the same person who holds the app token (by checking IP addresses, etc) and drops a session cookie in the browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 3 (SSO):&lt;/strong&gt; The browser is redirected to the target Web Application. Because the cookie now exists, the Web App can sign the user in automatically via standard OpenID Connect without asking the user to type in a password again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;“Next time on…”&lt;/h2&gt;
&lt;p&gt;In the interests of keeping things digestible, I will publish two follow up articles in the coming weeks.&lt;/p&gt;
&lt;p&gt;The first will &lt;a href=&quot;https://developer.pingidentity.com/blog/implementing-secure-session-transfer-a-field-guide-using-the-pingone-platform/&quot;&gt;explore the technical implementation&lt;/a&gt; of this pattern end-to-end, using PingOne SSO and PingOne DaVinci, together with a simple Android application to show it all working.&lt;/p&gt;
&lt;p&gt;The second will dive a little deeper into some of the security implications of this approach, highlight the advantages, point out a number of inherent drawbacks and also suggest further steps that can be taken to harden the implementation.&lt;/p&gt;
&lt;h2&gt;References and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8252&quot;&gt;RFC 8252 — OAuth 2.0 for Native Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.researchgate.net/publication/363052475_COOKIEGRAPH_Measuring_and_Countering_First-Party_Tracking_Cookies&quot;&gt;Urban et al., “Beyond the Front Page: Measuring and Countering First-Party Cookie Tracking,” in Proc. of the 2020 World Wide Web Conference (WWW).&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc9126&quot;&gt;RFC 9126 — OAuth 2.0 Pushed Authorization Requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CiXag4ms_27rSQ8.webp" medium="image" type="image/webp" width="2386" height="1256"/></item><item><title>Securing MCP Servers with Ping’s MCP Gateway</title><link>https://developer.pingidentity.com/blog/securing-mcp-servers-with-pings-mcp-gateway/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/securing-mcp-servers-with-pings-mcp-gateway/</guid><description>Building MCP servers that expose your business logic to AI agents? Discover how PingGateway can act as your security bouncer, handling OAuth validation, auditing, rate limiting, and policy enforcement without adding a single line of security code to your servers.</description><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You’ve been shipping &lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot;&gt;&lt;strong&gt;MCP servers&lt;/strong&gt;&lt;/a&gt; and each one comes with its own OAuth handler, its own scope-checking logic, its own audit hooks. Security code that should be administered centrally ends up copy-pasted across every server you build.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/mcp/&quot;&gt;&lt;strong&gt;MCP security gateway&lt;/strong&gt;&lt;/a&gt; capabilities in &lt;a href=&quot;https://www.pingidentity.com/en/product/pinggateway.html&quot;&gt;&lt;strong&gt;PingGateway&lt;/strong&gt;&lt;/a&gt; solve this by acting as a security bouncer in front of your MCP servers. The bouncer checks credentials at the door, enforces the rules, and logs everything — so your servers never have to.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can find a deeper dive into MCP’s architecture, workflow, and security model in the &lt;a href=&quot;https://developer.pingidentity.com/identity-for-ai/agents/idai-what-is-mcp.html&quot;&gt;Ping Identity MCP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Where PingGateway sits as your MCP security gateway&lt;/h2&gt;
&lt;p&gt;In a typical setup, &lt;strong&gt;PingGateway as an MCP security gateway&lt;/strong&gt; sits between the MCP client (the AI agent) and your MCP server.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;What PingGateway adds for MCP servers&lt;/h2&gt;
&lt;p&gt;By fronting your MCP server, PingGateway validates that requests are syntactically correct MCP, enforces operational requirements like audit and throttling, enforces authorization of the request, and mediates security tokens before forwarding to the MCP server.&lt;/p&gt;
&lt;p&gt;Your server continues to implement tools and domain behavior; the gateway provides the surrounding security and operational controls.&lt;/p&gt;
&lt;h2&gt;What capabilities does PingGateway provide?&lt;/h2&gt;
&lt;p&gt;PingGateway extends its proven API security stack with &lt;strong&gt;MCP-aware capabilities&lt;/strong&gt; you configure once and reuse across every server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;McpValidationFilter&lt;/strong&gt;: Validates &lt;strong&gt;Origin&lt;/strong&gt; / &lt;strong&gt;Accept&lt;/strong&gt; headers and MCP / JSON‑RPC format and populates an &lt;strong&gt;McpContext&lt;/strong&gt; so subsequent filters (throttling, policy, metrics) can inspect method and tool names.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;McpProtectionFilter&lt;/strong&gt;: Serves &lt;strong&gt;OAuth 2.0 Protected Resource Metadata&lt;/strong&gt; (&lt;code&gt;/.well-known/oauth-protected-resource/...&lt;/code&gt;) on behalf of your MCP endpoint, ensures the access token’s &lt;strong&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/strong&gt; matches the configured &lt;code&gt;resourceId&lt;/code&gt; and uses PingGateway’s &lt;strong&gt;OAuth 2.0 resource server&lt;/strong&gt; capabilities to validate tokens and enforce scopes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;McpAuditFilter:&lt;/strong&gt; Emits &lt;strong&gt;MCP‑specific audit events&lt;/strong&gt; (who called which tool, where, and with what result)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP metrics&lt;/strong&gt;: Exposes &lt;strong&gt;Prometheus metrics&lt;/strong&gt; per MCP method/tool (counts, latencies, errors).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP throttling&lt;/strong&gt;: Rate limits access per route, per tool, per client or per identity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Seeing it in action: the “unauthenticated” request&lt;/h2&gt;
&lt;p&gt;The gateway’s job is to stop what shouldn’t get through — and wave through what should. If the MCP client (AI agent) sends a request without the proper identity tokens required by the Ping Identity platform, the gateway will block it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The situation: missing authorization header.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An AI agent sends a tool call without an &lt;code&gt;Authorization&lt;/code&gt; header:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;POST /mcp/v1/call_tool&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;tools/call&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;params&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;access_customer_database&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;arguments&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;customer_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;12345&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;401 Unauthorized&lt;/code&gt; — rejected by PingGateway before the request ever reaches your server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;With valid credentials, the gateway allows the request through.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The same agent retries with a valid, scoped Bearer token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; /mcp/v1/call_tool&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Bearer &amp;lt;valid-access-token&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;method&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;tools/call&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;params&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;access_customer_database&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;arguments&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;customer_id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;12345&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;200 OK&lt;/code&gt; — PingGateway validates the token, confirms scope, logs the event, and forwards the request to your MCP server. Your server never saw the first request at all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Benefits for the MCP server developer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP security shouldn’t be implemented in each MCP server’s codebase&lt;/strong&gt;&lt;br /&gt;
Without a gateway, each MCP server grows its own OAuth, scopes, auditing, throttling, and risk logic.&lt;br /&gt;
With MCP Security Gateway, a &lt;strong&gt;standard PingGateway route&lt;/strong&gt; in front of each MCP server centralizes that logic; you reuse the pattern for every new server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comply with MCP authorization by configuration, not code&lt;/strong&gt;&lt;br /&gt;
MCP requires &lt;strong&gt;OAuth 2.0 Protected Resource Metadata&lt;/strong&gt; so agents know which AS to use and which resource they call. PingGateway serves this metadata and validates tokens according to it, so you configure security once instead of coding it per server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gain observability “for free”&lt;/strong&gt;&lt;br /&gt;
Audit logs and metrics at the gateway show &lt;strong&gt;which tools are used, by whom, and how often&lt;/strong&gt;, without extra instrumentation in your MCP code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect MCP servers from noisy or malicious agents&lt;/strong&gt;&lt;br /&gt;
Central throttling and policy/risk rules ensure your servers only see pre‑validated, rate‑limited, authorized requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrate legacy or sensitive backends cleanly&lt;/strong&gt;&lt;br /&gt;
Use &lt;strong&gt;token mediation&lt;/strong&gt; at the gateway to map agent tokens to whatever your backends expect (different scopes, JWT formats, API keys, mTLS), keeping your MCP server’s backend calls simple and aligned with existing security models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The gateway acts as the “bouncer,” ensuring the server only spends resources processing requests that are properly formatted, authenticated, and authorized.&lt;/p&gt;
&lt;h2&gt;Get started!&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/mcp/&quot;&gt;MCP security gateway tutorial&lt;/a&gt; walks you through the complete setup. Download the sample MCP agent and server (or bring your own), insert PingGateway in front of your MCP server, and enable MCP processing and authorization in the gateway configuration.&lt;/p&gt;
&lt;p&gt;MCP exposes your business capabilities to AI agents. &lt;a href=&quot;https://www.pingidentity.com/en/product/pinggateway.html&quot;&gt;&lt;strong&gt;PingGateway&lt;/strong&gt;&lt;/a&gt; makes sure only the right agents — with the right credentials and the right permissions — ever reach them.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Get started with the &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/mcp/&quot;&gt;MCP security gateway tutorial&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Visit the &lt;a href=&quot;https://developer.pingidentity.com/identity-for-ai/agents/idai-what-is-mcp.html&quot;&gt;MCP documentation on the Ping Identity developer portal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check out developer resources for all Ping Identity products at &lt;a href=&quot;https://developer.pingidentity.com/&quot;&gt;developer.pingidentity.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity Developer Community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.1PUsVrVE_Z16vVkj.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>We Rebuilt the PingOne API Docs. Here’s What’s New.</title><link>https://developer.pingidentity.com/blog/we-rebuilt-the-pingone-api-docs/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/we-rebuilt-the-pingone-api-docs/</guid><description>A walkthrough of the new PingOne API docs experience, including faster navigation, in-browser request testing, and smarter search.</description><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;If you build integrations, you probably spend a lot of time staring at API documentation. So when we decided to overhaul the PingOne API docs, our goal was simple: get out of your way and make it easier for you to actually test and write code.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/&quot;&gt;new site&lt;/a&gt; officially went live in early January. Here’s a quick rundown of what changed and how to use the new features.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;It’s a lot faster and easier to navigate&lt;/h2&gt;
&lt;p&gt;The most obvious improvement is performance. The site loads faster, the pages are lighter, and the layout now matches the rest of the &lt;a href=&quot;https://docs.pingidentity.com&quot;&gt;Ping Identity documentation&lt;/a&gt; ecosystem.&lt;/p&gt;
&lt;p&gt;We also reorganized the navigation so you don’t have to dig through endless menus to find helpful information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New to PingOne?&lt;/strong&gt; We have plenty of resources to help you get up and running quickly. Jump into our &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/getting-started/&quot;&gt;&lt;em&gt;Getting Started&lt;/em&gt;&lt;/a&gt; guide, explore &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/foundations/&quot;&gt;&lt;em&gt;Foundations&lt;/em&gt;&lt;/a&gt;, or follow along with step-by-step tutorials in the &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/workflow-library/&quot;&gt;&lt;em&gt;Use Case Library&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn about APIs:&lt;/strong&gt; You can dive straight into the PingOne Platform &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/platform/&quot;&gt;SSO&lt;/a&gt; or &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/auth/&quot;&gt;Auth&lt;/a&gt; APIs to see detailed info on all endpoints, which are logically grouped by functional area.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Universal services:&lt;/strong&gt; These now have their own dedicated section, making it clearer how they fit into the wider Ping Identity portfolio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart search:&lt;/strong&gt; If you still can’t find what you need, the search bar now uses the same AI-assisted backend as our other &lt;a href=&quot;https://docs.pingidentity.com&quot;&gt;doc sites&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Test endpoints in your browser&lt;/h2&gt;
&lt;p&gt;For every endpoint, you’ll see example requests and responses in multiple programming languages. More importantly, we completely rebuilt the &lt;strong&gt;Try Request&lt;/strong&gt; feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Live testing:&lt;/strong&gt; Plug in your parameters, hit the button, and get real API responses back immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent parameters:&lt;/strong&gt; You no longer have to copy and paste the same environment or user IDs over and over. Your parameters carry over from one API call to the next. This includes access tokens, so you don’t have to constantly re-authenticate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure local storage:&lt;/strong&gt; To keep things safe, those variables only live in your web browser’s session storage. The second you close the tab, they’re gone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;Update your bookmarks&lt;/h2&gt;
&lt;p&gt;To keep things simple, all developer-focused content, including cloud and on-premise APIs, now lives in one place: the &lt;a href=&quot;https://developer.pingidentity.com&quot;&gt;Developer Portal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Check out the new PingOne API docs at &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/&quot;&gt;&lt;strong&gt;developer.pingidentity.com/pingone-api&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Let us know what you think&lt;/h2&gt;
&lt;p&gt;This launch was a massive effort, but the work doesn’t stop here. Documentation is never really “finished.”&lt;/p&gt;
&lt;p&gt;Give the new site a spin. If you run into a weird bug, see a typo, or have any suggestions, let us know by clicking the feedback icon in the top-right corner of each page.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Visit our new &lt;a href=&quot;https://developer.pingidentity.com/pingone-api/&quot;&gt;PingOne API documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check out developer resources for all Ping Identity products at &lt;a href=&quot;https://developer.pingidentity.com/&quot;&gt;developer.pingidentity.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity Developer Community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.BOqoeYJk_ZRA2Fe.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Introducing the PingOne Advanced Identity Cloud (AIC) MCP Server</title><link>https://developer.pingidentity.com/blog/introducing-the-aic-mcp-server/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/introducing-the-aic-mcp-server/</guid><description>Introducing the AIC MCP Server: Connect Your Identity Infrastructure to AI Agents.</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Developers have already discovered that LLMs (such as Claude, Gemini, GPT, Grok, and others) accelerate workflows across coding, debugging, and operations. A real breakthrough has been the ability to create custom agents within the IDE. Now developers can write specialized agent workflows that combine reasoning, tooling, and domain logic to solve problems autonomously.&lt;/p&gt;
&lt;p&gt;To help these agents work effectively, we need a bridge between the AI and your actual services. That is where &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;Today, we are excited to introduce the &lt;strong&gt;AIC MCP Server&lt;/strong&gt;. This tool connects your PingOne Advanced Identity Cloud (AIC) environment directly to the AI agents you use every day in IDEs like Cursor, VS Code, and GitHub Copilot, or command-line tools like Claude Code and Gemini CLI.&lt;/p&gt;
&lt;h2&gt;What Exactly is an MCP Server?&lt;/h2&gt;
&lt;p&gt;Think of the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; as a universal remote for AI. In the past, every AI platform had to build its own unique way to talk to different databases or services. MCP changes that by creating a standard “contract.”&lt;/p&gt;
&lt;p&gt;It allows an AI agent to see a list of available tools, understand what they do, and know exactly how to call them. When you use an MCP server, you aren’t just giving an AI a window into your data; you’re giving it a specialized “hand” to perform tasks for you.&lt;/p&gt;
&lt;p&gt;You can find a more detailed look into MCP’s architecture, workflow, and security model in the &lt;a href=&quot;https://developer.pingidentity.com/identity-for-ai/agents/idai-what-is-mcp.html&quot;&gt;Ping Identity MCP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How the AIC MCP Server Works&lt;/h2&gt;
&lt;p&gt;PingOne Advanced Identity Cloud (AIC) is Ping Identity’s modern, single-tenant identity platform. It manages users, authentication flows, roles, policies, audit logs, and customer-facing themes. If you’re running an identity operation, AIC is where the configuration lives.&lt;/p&gt;
&lt;p&gt;The AIC MCP server is an open-source TypeScript tool, distributed through npm, which exposes 40+ tools that wrap AIC’s REST APIs and packages them as MCP-compatible functions. Instead of manually navigating the console or hand-crafting API requests, you describe what you want in natural language. Your agent translates that to the right MCP tools, the MCP server handles the API calls and authentication, and returns results that the agent can reform into natural language.&lt;/p&gt;
&lt;p&gt;Here’s the practical difference:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Without MCP&lt;/th&gt;
&lt;th&gt;With AIC MCP Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Find failed logins from 9 PM – 11 PM yesterday&lt;/td&gt;
&lt;td&gt;20 minutes (console navigation, log filtering, export)&lt;/td&gt;
&lt;td&gt;1 minute (“Show me authentication failures from 9 PM – 11 PM yesterday”)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a new role with specific permissions&lt;/td&gt;
&lt;td&gt;15 minutes (console form, multi-step workflow)&lt;/td&gt;
&lt;td&gt;2 minutes (“Create a role called product-admin with these permissions: …”)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List all users in a specific group&lt;/td&gt;
&lt;td&gt;10 minutes (API docs, construct request, parse response)&lt;/td&gt;
&lt;td&gt;30 seconds (“Which users are in the engineers group?”)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Why This Matters: MCP Adoption Is Evolving&lt;/h2&gt;
&lt;p&gt;MCP has become the standard for AI-to-infrastructure integration. Popular developer tools (such as Claude Desktop, Cursor, GitHub Copilot, VS Code, Gemini CLI) all now support MCP. Developers are moving from being software engineers or technical admins to being agent orchestrators.&lt;/p&gt;
&lt;p&gt;Identity and access management (IAM) is a perfect fit for AI assistance. Configuration can be repetitive. Audits demand detailed log parsing. Bulk operations can be tedious. Configuration updates may take a while when navigating multiple screens and modals in the web console. IAM is also not an isolated capability, often connected to other systems that benefit from being managed as a group. In this way, MCP clients allow connection of multiple MCP servers from different providers to bridge multi-service, multi-product workflows. The more systems connected, the more autonomy an agent has, the more the user can delegate.&lt;/p&gt;
&lt;p&gt;For developers, this means faster operations. Less manual console navigation. Less copying and pasting IDs between tabs. Reduced context-switching.&lt;/p&gt;
&lt;h2&gt;Core Capabilities: 35+ Tools Across Five Categories&lt;/h2&gt;
&lt;p&gt;The AIC MCP server exposes 35+ tools organized into five functional areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Managed Objects:&lt;/strong&gt; Perform “search, create, and update” tasks for users, roles, groups, and organizations. (for example, &lt;em&gt;“Find all users with an @acme.com email.”&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Themes &amp;amp; Branding:&lt;/strong&gt; Update your login pages, logos, and brand colors across different regions programmatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logging &amp;amp; Audits:&lt;/strong&gt; Query activity logs by time, user, or error type—perfect for quick security check-ups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets &amp;amp; Variables:&lt;/strong&gt; Securely manage API keys and environment variables without having to hard-code them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication Journeys:&lt;/strong&gt; Build and test complex login flows (journeys) and the scripts that power them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The server exposes many tools providing broad capability, with every action authenticated using OAuth 2.0 with PKCE and tied to your user identity in the audit log.&lt;/p&gt;
&lt;h2&gt;Getting Started: Hands-On Walkthrough&lt;/h2&gt;
&lt;p&gt;Getting the AIC MCP Server running takes 5 minutes. You’ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Sandbox or Development AIC tenant.&lt;/li&gt;
&lt;li&gt;Node.js 18+ on your machine (or Docker).&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client (Claude Desktop is recommended).&lt;/li&gt;
&lt;li&gt;5 minutes of setup time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1: Configure Your MCP Client&lt;/h3&gt;
&lt;p&gt;Find your AIC tenant’s base URL in the console (for example, &lt;code&gt;my-tenant.forgeblocks.com&lt;/code&gt;). Then add the configuration to your MCP client’s config file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;aic-mcp-server&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;-y&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;@ping-identity/aic-mcp-server&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;env&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;AIC_BASE_URL&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;your-tenant.forgeblocks.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;your-tenant.forgeblocks.com&lt;/code&gt; with your actual tenant URL (no &lt;code&gt;https://&lt;/code&gt; prefix).&lt;/p&gt;
&lt;h3&gt;Step 2: Authenticate&lt;/h3&gt;
&lt;p&gt;Restart your AI client. When you first use a tool, the server will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open your browser to your AIC login screen.&lt;/li&gt;
&lt;li&gt;Ask you to log in with your AIC admin credentials as an administrator.&lt;/li&gt;
&lt;li&gt;Show you the permissions the server needs (scopes like &lt;code&gt;fr:idm:\*&lt;/code&gt;, &lt;code&gt;fr:idc:monitoring:\*&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;After you’re authenticated, securely save your token in your OS keychain (where available).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a one-time setup for the session. The token stays in your keychain and can be reused until expiry or ephemerally if using the Docker image. Subsequent tool calls reuse the cached token, if it is still valid.&lt;/p&gt;
&lt;h3&gt;Step 3: Make Your First Tool Call&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Count all users in the system and show me the total.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behind the scenes, your agent will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understand the available tools provided by the MCP server.&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;listManagedObjects&lt;/code&gt; tool to discover object types.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;queryManagedObjects&lt;/code&gt; to fetch users.&lt;/li&gt;
&lt;li&gt;Return the count in plain English.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expected response: “There are 247 users in your AIC tenant.”&lt;/p&gt;
&lt;h2&gt;Real-World Use Case: Debugging Authentication Issues&lt;/h2&gt;
&lt;p&gt;It’s 8 AM. A high-priority bug report just landed: “A recent change to the primary user authentication journey has caused failures in overnight tests.” You need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Analyze the bug&lt;/strong&gt; and map it to the existing auth journey.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provision test identities&lt;/strong&gt; that match the specific failure criteria.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trace the logs&lt;/strong&gt; to find the exact line of code or configuration causing the break.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Patch the journey&lt;/strong&gt; and verify the fix immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Without the server:&lt;/strong&gt; 45 minutes of context switching (Jira, documentation, console, log aggregators, scripts).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With the AIC MCP Server (and other MCP servers that interact with ticket systems and UX):&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1. Analyze the Bug and Journey&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your prompt to Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Read bug report AUTH-802 and show me the current &apos;SSO-Onboarding&apos; journey configuration.  Compare with recent closed tickets that may have caused a breaking change.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Action:&lt;/strong&gt; Your agent fetches the ticket details using the ticket system MCP server and pulls the journey configuration. It identifies a probable failure that happens during a user lookup step from an external data source.
&lt;strong&gt;Time to insight:&lt;/strong&gt; 30 - 60 seconds.&lt;/p&gt;
&lt;h3&gt;2. Reproduce with Test Users&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your prompt to Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Create three test users: two with valid metadata in the external data source and one with invalid data to replicate the reported error.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Action:&lt;/strong&gt; Agent calls &lt;code&gt;createManagedObject&lt;/code&gt; and tools related to the external data source. It returns key details for fresh accounts tailored to your debugging needs.
&lt;strong&gt;Result:&lt;/strong&gt; Environment ready in 20 seconds.&lt;/p&gt;
&lt;h3&gt;3. Trace Logs &amp;amp; Identify the Root Cause&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your prompt to Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Run a test authentication for the user with invalid data. Show me the logs and highlight any stack traces.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Action:&lt;/strong&gt; Agent triggers a mock login and calls &lt;code&gt;queryLogs&lt;/code&gt;.
&lt;strong&gt;Result:&lt;/strong&gt; “Found an error relating to user lookup. The journey isn’t handling user lookup where there is invalid data correctly.”
&lt;strong&gt;Time to discovery:&lt;/strong&gt; 5 minutes.&lt;/p&gt;
&lt;h3&gt;4. Update &amp;amp; Re-test&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your prompt to Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Update the &apos;SSO-Onboarding&apos; journey to include appropriate error handling when user data lookups fail, then re-test with the same user.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Action:&lt;/strong&gt; Agent calls &lt;code&gt;saveJourney&lt;/code&gt; with the fix and re-runs the authentication trace.
&lt;strong&gt;Result:&lt;/strong&gt; “Test passed. Redirect successful. Journey updated in Dev environment.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Total time saved:&lt;/strong&gt; 30 - 45 minutes of manual work → less than 10 minutes of natural language queries. The workflow can be packaged up into an agent to save more time later. The difference compounds across your team.&lt;/p&gt;
&lt;h2&gt;Security and Caveats&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.0 Authentication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every tool call is authenticated with your AIC credentials using OAuth 2.0 (PKCE flow). Your password is never stored. The resulting access token is saved securely in your OS keychain—the same place your browser stores credentials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit Trail&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All actions—queries, creates, updates, deletes—appear in your AIC audit log tied to your user identity and are traceable as MCP-based interactions. There’s full traceability for compliance and security reviews.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sandbox and Development Only&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Currently, the server works with Sandbox and Development AIC tenants only. Production support is on the roadmap. This ensures you can safely experiment and iterate without risk to live systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent Permissions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only use the MCP server with AI agents you trust. The agent has the same permissions as your authenticated user. If you’re an AIC admin, the agent is an admin. Always review agent prompts and tool calls before deploying them to production workflows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open Source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The server is Apache 2.0 licensed and open-source on GitHub. Review the code, audit the dependencies, and contribute. The entire codebase is at &lt;a href=&quot;https://github.com/pingidentity/aic-mcp-server&quot;&gt;github.com/pingidentity/aic-mcp-server&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Next Steps: Try It Today, Give us Feedback&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Run your first agent workflow.&lt;/li&gt;
&lt;li&gt;Consider your developer workflows, how can the MCP server be used to turn repetitive or menial tasks into automated, intelligent workflows?&lt;/li&gt;
&lt;li&gt;Share your use cases and workflows with the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you find issues or would like to see enhancements, &lt;a href=&quot;https://github.com/pingidentity/aic-mcp-server/issues&quot;&gt;raise an issue on the code repository&lt;/a&gt;. We’re listening!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AIC MCP Server is in early release. Your feedback shapes what comes next!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href=&quot;https://github.com/pingidentity/aic-mcp-server&quot;&gt;https://github.com/pingidentity/aic-mcp-server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NPM Package: &lt;a href=&quot;https://www.npmjs.com/package/@ping-identity/aic-mcp-server&quot;&gt;https://www.npmjs.com/package/@ping-identity/aic-mcp-server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingOne AIC Documentation: &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/&quot;&gt;https://docs.pingidentity.com/pingoneaic/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Model Context Protocol (MCP) Specification: &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25&quot;&gt;https://modelcontextprotocol.io/specification/2025-11-25&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OAuth 2.0 PKCE (RFC 7636): &lt;a href=&quot;https://tools.ietf.org/html/rfc7636&quot;&gt;https://tools.ietf.org/html/rfc7636&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.KoQ5fhOb_ZF6ODT.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Introducing the PingOne MCP server</title><link>https://developer.pingidentity.com/blog/introducing-the-pingone-mcp-server/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/introducing-the-pingone-mcp-server/</guid><description>The PingOne MCP Server brings your identity infrastructure into your development workflow. Configure environments, applications, and populations without leaving your IDE.</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Developers building applications using PingOne eventually hit the same friction point. Application code is in the IDE, while application configuration, audit logs, policies, and user journey flows are managed in the web console.&lt;/p&gt;
&lt;p&gt;That context switch is the problem, not the task itself. Registering an application in PingOne or spinning up a new environment for development is straightforward. The cost is the frequent interruption leading to a break in flow, tab switching, and copying and pasting IDs.&lt;/p&gt;
&lt;p&gt;Although frequent switching between IDE and the web console has been well accepted over the years, the expectations of application developers is shifting. Application developers are shifting to AI tools and agentic development practices. Generative code experiences such as VS Code Copilot, Claude Desktop, Claude Code, Gemini CLI, and GPT Codex are changing the ways that developers interact with their application code, and they’re changing the way developers interact with online services.&lt;/p&gt;
&lt;p&gt;The PingOne MCP Server reduces the gap. It connects your PingOne tenant directly to MCP-compatible generative AI IDEs and CLIs. Instead of leaving your editor, you describe what you need from your application integration in natural language, and your agent handles the PingOne API calls, returns the IDs and configuration details you need, and lets you keep iterating on application code.&lt;/p&gt;
&lt;p&gt;This isn’t about replacing the PingOne console for operational work. It’s about making identity platform configuration a first-class citizen of the development cycle, available in the same context where you write code, run tests, and ship features.&lt;/p&gt;
&lt;h2&gt;What Is MCP?&lt;/h2&gt;
&lt;p&gt;System configuration living outside the development cycle isn’t unique to PingOne. But it’s a fundamental challenge in building agentic systems: how do AI agents and IDE AI chat systems connect to the external services they need to act on?&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt;, originally developed by Anthropic, provides a specification for the interactions between AI systems (the abstract world of “intent”) and today’s systems (the concrete world of deterministic input/output). Before MCP, every integration between an AI agent and an external tool required custom code—bespoke connectors, point-in-time maintenance, duplicated effort across teams. MCP standardizes the connection layer, providing a “USB style” connector interface so that any MCP-compatible client (VS Code Copilot, Claude Desktop, Cursor, and others) can discover and invoke any MCP server’s capabilities using a common protocol, without needing custom code on either side.&lt;/p&gt;
&lt;p&gt;In practical terms, an MCP server currently exposes three types of capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; (executable actions the agent can invoke)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resources&lt;/strong&gt; (contextual data to inform reasoning)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompts&lt;/strong&gt; (templated workflows)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent discovers tools at runtime, selects what it needs, and calls them through JSON-RPC messages. Prompts and resources are loaded at the user’s request.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can find a deeper dive into MCP’s architecture, workflow, and security model in the &lt;a href=&quot;https://developer.pingidentity.com/identity-for-ai/agents/idai-what-is-mcp.html&quot;&gt;Ping Identity MCP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is the PingOne MCP Server?&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server&quot;&gt;PingOne MCP Server&lt;/a&gt; implements the MCP standard for PingOne’s Management API, exposing 14 tools across 4 capability areas, and represents the start of the journey in producing useful tools to accelerate developer workflows.&lt;/p&gt;
&lt;p&gt;In practical terms: your AI agent in your IDE can now interact with your PingOne tenant the same way it interacts with your filesystem, your terminal, or your version control. It becomes part of the same conversation as your application code.&lt;/p&gt;
&lt;p&gt;The initial set of tools cover what developers need most when building on PingOne:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Applications&lt;/strong&gt;: List, retrieve, create, and update OIDC/OAuth 2.0 applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environments&lt;/strong&gt;: List, retrieve, create, and update PingOne environments; manage services (such as MFA and DaVinci)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Populations&lt;/strong&gt;: List, retrieve, create, and update user populations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directory&lt;/strong&gt;: Generate per-day identity trend reports.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All operations execute as the authenticated user and are logged in PingOne’s audit trail. The agent never sees your access tokens or sign-on credentials; the MCP server handles authentication entirely, returning only the data the agent needs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Missing something you need? Get in touch! Raise an &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/issues/new&quot;&gt;enhancement request on the code repository&lt;/a&gt;. Your feedback helps drive what we do next.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Getting Started: From Zero to First Tool Call&lt;/h2&gt;
&lt;p&gt;To get started you need a PingOne account (&lt;a href=&quot;https://www.pingidentity.com/en/try-ping.html&quot;&gt;Try Ping here&lt;/a&gt;), an MCP-compatible client (VS Code with Copilot, Cursor, Claude Desktop, or similar).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the adventurous&lt;/strong&gt;, in your favourite agent IDE, try:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Help me install and configure the PingOne MCP server from github.com/pingidentity/pingone-mcp-server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Otherwise, follow the instructions below.&lt;/p&gt;
&lt;h3&gt;1. Install the Server&lt;/h3&gt;
&lt;p&gt;The easiest path on macOS or Linux is Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; tap&lt;/span&gt;&lt;span&gt; pingidentity/tap&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; pingone-mcp-server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pingone-mcp-server&lt;/span&gt;&lt;span&gt; --version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For Windows, the MCP server is built as an executable file, ready to download from the &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/releases&quot;&gt;project releases&lt;/a&gt;. See the project &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server?tab=readme-ov-file#install-the-mcp-server&quot;&gt;README&lt;/a&gt; for full details for Windows installation and other options.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. Set Up a Worker Application in PingOne&lt;/h3&gt;
&lt;p&gt;The server needs credentials. Create a worker application in your PingOne tenant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sign on to the PingOne Administration Console.&lt;/li&gt;
&lt;li&gt;Open the environment for your Administrator users.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Applications&lt;/strong&gt; &amp;gt; &lt;strong&gt;Add (+)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it (for example, “PingOne MCP Server”) and choose type &lt;strong&gt;Worker&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Configuration&lt;/strong&gt; and configure OAuth 2.0 settings:
&lt;ul&gt;
&lt;li&gt;Set response type to &lt;strong&gt;Code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set grant type to &lt;strong&gt;Authorization Code&lt;/strong&gt; (uncheck all others, including &lt;strong&gt;Client Credentials&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;PKCE Enforcement&lt;/strong&gt; to &lt;strong&gt;S256_REQUIRED&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Refresh Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set redirect URI to &lt;code&gt;http://127.0.0.1:7464/callback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the Token Endpoint Authentication Method to &lt;strong&gt;None&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save and capture your &lt;strong&gt;Environment ID&lt;/strong&gt; and &lt;strong&gt;Client ID&lt;/strong&gt; from the application details.&lt;/li&gt;
&lt;li&gt;Finally, remember to enable the MCP server application!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;Although the example here uses the ‘Authorization Code’ grant type, ‘Device Code’ is also available. You can find more details in &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/blob/main/docs/setup-pingone-worker-application.md&quot;&gt;Setting Up PingOne Worker Applications for MCP Server&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;3. Configure Your MCP Client&lt;/h3&gt;
&lt;p&gt;Once installed, add the server to your MCP client’s configuration. For VS Code with Copilot Chat:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;servers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;pingOne&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;pingone-mcp-server&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;run&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        // &quot;--disable-read-only&quot; // uncomment to enable write tools&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;env&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;PINGONE_MCP_ENVIRONMENT_ID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;your-environment-id-uuid&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;PINGONE_AUTHORIZATION_CODE_CLIENT_ID&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;your-client-id-uuid&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;PINGONE_ROOT_DOMAIN&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;pingone.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If your tenant is in a different region, use &lt;em&gt;pingone.ca&lt;/em&gt;, &lt;em&gt;pingone.eu&lt;/em&gt;, &lt;em&gt;pingone.com.au&lt;/em&gt;, &lt;em&gt;pingone.sg&lt;/em&gt; or &lt;em&gt;pingone.asia&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To install to other IDEs, including Claude Code, Gemini and Cursor, refer to the project &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That’s it. On first tool use, your browser opens your PingOne administrator sign-on page. After you authenticate with your admin credentials, the server caches your token securely in your OS keychain (where available) or is cached ephemerally. Every subsequent tool call validates and reuses the same token.&lt;/p&gt;
&lt;h2&gt;Real-World Use Case: Prepare an Environment for App Development&lt;/h2&gt;
&lt;p&gt;You’re building a new application feature that requires its own isolated identity configuration: a dedicated, ephemeral sandbox environment, a test user population, and an OIDC application for the OAuth flow your code will use.&lt;/p&gt;
&lt;p&gt;Without the MCP server, you stop coding, switch to a browser, walk through the PingOne console to create the environment, toggle the services you need, create the population, register the application, copy the client ID and environment ID back into your project, and resume back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With the PingOne MCP Server, you stay in your IDE:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that write tools must be enabled using &lt;code&gt;--disable-read-only&lt;/code&gt; at startup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Understand what already exists:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;List my PingOne sandbox environments and show me which services are enabled in each.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent calls &lt;code&gt;list_environments&lt;/code&gt; and returns a structured overview. You can see your existing dev and staging environments, what’s running in each, and confirm there’s no overlap with what you’re about to create.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Create the new environment:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Create a new sandbox environment called &apos;feature-payments-dev&apos;.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Enable the MFA and DaVinci services.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent calls &lt;code&gt;create_environment&lt;/code&gt;, then &lt;code&gt;update_environment_services&lt;/code&gt; to add MFA and DaVinci, and returns the environment ID. You paste it into your &lt;code&gt;.env&lt;/code&gt; file without leaving the editor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Create the test population:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Create a population called &apos;Payments Test Users&apos; in the&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;feature-payments-dev environment.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent calls &lt;code&gt;create_population&lt;/code&gt;. Returns the population ID. You add it to your test harness configuration inline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Register the OIDC application:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Create an OIDC application in feature-payments-dev called&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&apos;Payments App&apos;. Use authorization code grant with PKCE enforced.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Redirect URI is http://localhost:3000/callback.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent calls &lt;code&gt;create_oidc_application&lt;/code&gt; with the correct grant type, PKCE settings, and redirect URI. It returns the client ID.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5 — Confirm and move on:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Give me a summary of the feature-payments-dev environment:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;environment ID, enabled services, population name and ID,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;and the Payments App client ID.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent returns a clean summary, and you’re ready to move on. The entire flow took less than three minutes without switching context once. All four operations are in your PingOne audit log, associated with your user identity.&lt;/p&gt;
&lt;p&gt;The process can be wrapped into a skill or an agent, and the rest of the team can benefit from the workflow.&lt;/p&gt;
&lt;h2&gt;Security Considerations&lt;/h2&gt;
&lt;p&gt;The most important thing to understand about the PingOne MCP Server’s security model: the agent is acting as you. The MCP server doesn’t have its own service account, its own permissions, or its own audit identity. When the agent calls a PingOne API through the MCP server, it calls it as your authenticated user account. Everything it does is bounded by the PingOne roles assigned to your admin account and actions are visible in the audit log, attributable to your identity.&lt;/p&gt;
&lt;p&gt;Other features include:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Production environment restrictions.&lt;/strong&gt; By default, all write actions are disabled on production environments. This is to protect against accidental or unintended configuration changes, even if your user has the ability to make changes to production configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write operations require an explicit decision when starting the MCP server.&lt;/strong&gt; Running the server without &lt;code&gt;--disable-read-only&lt;/code&gt; exposes only read tools. This is to allow you to explicitly opt in to write tools on your tenant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full audit trail.&lt;/strong&gt; Every API call from the server includes a session ID and transaction ID in the request headers. You can trace any change back to the MCP server session through PingOne’s audit log.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preview software.&lt;/strong&gt; This is an early release. APIs and behavior may change. Use caution when using the MCP server against important environments. Found a bug? &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/issues&quot;&gt;Open an issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open source.&lt;/strong&gt; Apache 2.0 licensed. Read the code, audit the dependencies, contribute a fix: &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server&quot;&gt;github.com/pingidentity/pingone-mcp-server&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Next Steps: Try It Today, Give us Feedback&lt;/h2&gt;
&lt;p&gt;The current 14 tools cover a subset of what developers may need when developing on PingOne. But the goal is broader: PingOne configuration should be as accessible from your IDE as your database schema, your API spec, or your CI/CD pipeline. Manual steps you need to take between your code and your identity configuration is a point of friction that can be improved upon.&lt;/p&gt;
&lt;p&gt;Near-term additions include Docker support for containerized workflows and simplified installation, expanded tool coverage across more PingOne APIs, and support for upcoming MCP specification changes as the standard evolves.&lt;/p&gt;
&lt;p&gt;The roadmap is shaped by what you actually need. If there’s a specific tool or capability that would close a gap in your workflow, &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/issues&quot;&gt;raise an enhancement request&lt;/a&gt; or start a discussion in the repository.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Get a &lt;a href=&quot;https://www.pingidentity.com/en/try-ping.html&quot;&gt;PingOne trial account&lt;/a&gt;&lt;/strong&gt; if you don’t have one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install and follow the project &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server&quot;&gt;README&lt;/a&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Think about the identity configuration steps in your current developer workflow. Where does context-switching slow you down?&lt;/li&gt;
&lt;li&gt;Share what you build with the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your feedback shapes what comes next. Try it today and give us feedback on what you want to build.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PingOne MCP Server code repository: &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server&quot;&gt;https://github.com/pingidentity/pingone-mcp-server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingOne MCP Server releases: &lt;a href=&quot;https://github.com/pingidentity/pingone-mcp-server/releases&quot;&gt;https://github.com/pingidentity/pingone-mcp-server/releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingOne documentation: &lt;a href=&quot;https://docs.pingidentity.com/pingone/latest/&quot;&gt;https://docs.pingidentity.com/pingone/latest/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Model Context Protocol (MCP) specification: &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25&quot;&gt;https://modelcontextprotocol.io/specification/2025-11-25&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingOne Management API: &lt;a href=&quot;https://apidocs.pingidentity.com/pingone/platform/v1/api/&quot;&gt;https://apidocs.pingidentity.com/pingone/platform/v1/api/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OAuth 2.0 PKCE (RFC 7636): &lt;a href=&quot;https://tools.ietf.org/html/rfc7636&quot;&gt;https://tools.ietf.org/html/rfc7636&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Homebrew Tap: &lt;a href=&quot;https://github.com/pingidentity/homebrew-tap&quot;&gt;https://github.com/pingidentity/homebrew-tap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.5FanacXr_Z1RkAVD.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Rapidly Deploying PingGateway with PingOne Advanced Identity Cloud (P1AIC)</title><link>https://developer.pingidentity.com/blog/rapidly-deploying-pinggateway-with-pingone-advanced-identity-cloud-p1aic/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/rapidly-deploying-pinggateway-with-pingone-advanced-identity-cloud-p1aic/</guid><description>A step-by-step guide to rapidly deploying PingGateway in standalone mode and integrating it with PingOne Advanced Identity Cloud using CDSSO.</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@darinder.shokar/rapidly-deploying-pinggateway-with-pingone-advanced-identity-cloud-p1aic-a500d3734a63&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note: This blog applies to PingGateway versions from 2025 onwards. For earlier PingGateway (IG) releases, please refer to the original blog &lt;a href=&quot;https://medium.com/@darinder.shokar/forgerock-identity-gateway-ig-integration-with-the-new-forgerock-identity-cloud-bf87b0b25a08&quot;&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Integrating applications securely and efficiently with PingOne Advanced Identity Cloud (P1AIC) is a common requirement for developers and architects exploring modern identity platforms.&lt;/p&gt;
&lt;p&gt;This article demonstrates how to deploy PingGateway in standalone mode and protect a sample application using Cross-Domain Single Sign-On (CDSSO) with P1AIC — all via a single automation script.&lt;/p&gt;
&lt;p&gt;The objective is straightforward:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Enable new users to quickly stand up PingGateway, integrate it with P1AIC, and understand how CDSSO protection works end-to-end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This deployment is designed for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learning and enablement&lt;/li&gt;
&lt;li&gt;Proof-of-concept environments&lt;/li&gt;
&lt;li&gt;Developer experimentation&lt;/li&gt;
&lt;li&gt;Workshops and demonstrations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is not intended as production use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The repository containing all assets and the deployment script is located &lt;a href=&quot;https://github.com/forgerock-dss/PingGatewayIntegrationwithP1AIC-2025&quot;&gt;here&lt;/a&gt; on GitHub.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To get straight to deploying go here.&lt;/p&gt;
&lt;h2&gt;Scenario&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;“As a new customer, I want to understand PingGateway through hands-on experimentation — specifically how to configure it to protect an application via CDSSO with P1AIC.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This blog walks through that exact scenario.&lt;/p&gt;
&lt;h2&gt;What the Script Deploys&lt;/h2&gt;
&lt;p&gt;The repository includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;install_ping_gateway_P1AIC.sh&lt;/code&gt; — automation script&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin.json.HTTPS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin.json.HTTP_ONLY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static-resources.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdsso-idc.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When executed, the script provisions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A PingGateway standalone instance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A Sample Application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Route configurations that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reverse proxy static resources&lt;/li&gt;
&lt;li&gt;Protects &lt;code&gt;/home/cdsso&lt;/code&gt; via CDSSO with P1AIC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All components are deployed on the same host for simplicity.&lt;/p&gt;
&lt;h2&gt;Architecture Overview&lt;/h2&gt;
&lt;p&gt;PingGateway runs in &lt;strong&gt;Standalone Mode&lt;/strong&gt;, meaning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It does not require deployment into a servlet container.&lt;/li&gt;
&lt;li&gt;It runs directly from the extracted ZIP.&lt;/li&gt;
&lt;li&gt;It uses Vert.x internally to provide its runtime engine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a lightweight, fast-starting gateway instance suitable for development and experimentation.&lt;/p&gt;
&lt;p&gt;The end-to-end flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User accesses protected resource via PingGateway.&lt;/li&gt;
&lt;li&gt;PingGateway checks for a valid OIDC token.&lt;/li&gt;
&lt;li&gt;If no valid token is present, the browser is redirected to P1AIC.&lt;/li&gt;
&lt;li&gt;User authenticates with P1AIC.&lt;/li&gt;
&lt;li&gt;P1AIC redirects back to PingGateway.&lt;/li&gt;
&lt;li&gt;PingGateway validates the token and grants access to the sample application.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This demonstrates CDSSO in action, and described in more details below:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;To learn more about CDSSO check out &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/gateway-guide/cdsso.html&quot;&gt;this&lt;/a&gt; link&lt;/p&gt;
&lt;h2&gt;Script Execution&lt;/h2&gt;
&lt;h3&gt;Step 1 — Download Assets aond Configure Networking&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Download the ZIP from GitHub &lt;a href=&quot;https://github.com/forgerock-dss/PingGatewayIntegrationwithP1AIC-2025&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Down the PingGateway Standalone ZIP and PingGateway Sample Application JAR from &lt;a href=&quot;https://backstage.forgerock.com/downloads/browse/ig/featured&quot;&gt;here&lt;/a&gt; and place both files in the same directory as the script.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;By default PingGateway will be deployed against this host &lt;code&gt;pinggateway.test.com&lt;/code&gt; and the Sample app to &lt;code&gt;sample.test.com&lt;/code&gt;. Add both to your &lt;code&gt;/etc/hosts&lt;/code&gt; file. For example for IP 172.168.1.10 the hosts entry will look like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;172.168.1.10 pinggateway.test.com sample.test.com&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2 — Configure the Script&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Modify lines 11–30 in the &lt;code&gt;install_ping_gateway_P1AIC.sh&lt;/code&gt; script to match your environment&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 3 — Configure P1AIC&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Create a Test User&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to P1AIC.&lt;/li&gt;
&lt;li&gt;Select the appropriate realm.&lt;/li&gt;
&lt;li&gt;Navigate to Identities → Manage.&lt;/li&gt;
&lt;li&gt;Create a new user.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Create a Gateway Agent&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Navigate to Gateways and Agents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click New Gateway/Agent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select Identity Gateway.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the following and hit Save:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent ID (e.g. &lt;code&gt;pinggateway_agent_cdsso&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ensure these values match the script configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AGENT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Configure Redirect URIs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HTTPS:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pinggateway.test.com:9443/home/cdsso/redirect&quot;&gt;https://pinggateway.test.com:9443/home/cdsso/redirect&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;HTTP:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://pinggateway.test.com:9000/home/cdsso/redirect&quot;&gt;http://pinggateway.test.com:9000/home/cdsso/redirect&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Step 4 — Execute the Script&lt;/h3&gt;
&lt;p&gt;For P1AIC deployments, use HTTPS mode due to SameSite cookie requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run: &lt;code&gt;./install_ping_gateway_P1AIC.sh https&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For standalone PingAM deployments run &lt;code&gt;./install_ping_gateway_P1AIC.sh http&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 5 — Validate Deployment&lt;/h3&gt;
&lt;p&gt;After deployment, access:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pinggateway.test.com:9443/home/cdsso&quot;&gt;https://pinggateway.test.com:9443/home/cdsso&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You should be redirected to P1AIC for authentication. Upon successful login, the Sample Application page will be displayed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note: If you were already logged into P1AIC during setup, log out first or use a private browser window to prevent cookie conflicts.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h3&gt;Step 6— Stop/Start the PingGateway&lt;/h3&gt;
&lt;p&gt;To stop/start PingGateway and the Sample App use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stop Services: &lt;code&gt;./install_ping_gateway_P1AIC.sh stop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Start Services: &lt;code&gt;./install_ping_gateway_P1AIC.sh start&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Script Breakdown&lt;/h2&gt;
&lt;p&gt;The script performs the following high-level steps.&lt;/p&gt;
&lt;h3&gt;1. Environment Validation&lt;/h3&gt;
&lt;p&gt;The script validates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PingGateway ZIP is present&lt;/li&gt;
&lt;li&gt;Sample Application JAR is present&lt;/li&gt;
&lt;li&gt;Required commands are available (&lt;code&gt;unzip&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;The host can connect to the configured P1AIC endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Execution stops if validation fails.&lt;/p&gt;
&lt;h3&gt;2. Clean Installation Handling&lt;/h3&gt;
&lt;p&gt;If a previous installation exists:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PingGateway is stopped&lt;/li&gt;
&lt;li&gt;The sample application is stopped&lt;/li&gt;
&lt;li&gt;The installation directory is removed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This ensures clean redeployment.&lt;/p&gt;
&lt;h3&gt;3. PingGateway Deployment&lt;/h3&gt;
&lt;p&gt;The script:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extracts the PingGateway ZIP&lt;/li&gt;
&lt;li&gt;Performs a temporary start/stop cycle to initialise directories&lt;/li&gt;
&lt;li&gt;Creates required configuration folders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PingGateway is deployed in Production Mode by default.&lt;/p&gt;
&lt;h3&gt;4. Script Execution Mode&lt;/h3&gt;
&lt;p&gt;The script supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https mode (recommended for P1AIC)&lt;/li&gt;
&lt;li&gt;http mode (for standalone PingAM deployments)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When HTTPS mode is selected, a self-signed certificate is generated automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note — Self-signed certificates are for demonstration only and should not be used in production.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5. Sample Application Deployment&lt;/h3&gt;
&lt;p&gt;The sample application is deployed locally and started on the configured ports, providing a target application for PingGateway to protect.&lt;/p&gt;
&lt;h3&gt;6. Route Configuration&lt;/h3&gt;
&lt;p&gt;Two routes are created:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static-resources.json&lt;/code&gt; — Allows PingGateway to proxy static resources (e.g., CSS) without authentication. This improves performance and mirrors real-world deployment patterns.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdsso-idc.json&lt;/code&gt; — Protects the /home/cdsso context. It checks for a valid OIDC id_token, redirects unauthenticated users to P1AIC, validates tokens on return and grants access upon successful validation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This script provides a fast, repeatable way to deploy PingGateway locally and integrate it with PingOne Advanced Identity Cloud using CDSSO — all within minutes. It enables you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deploy PingGateway in standalone mode&lt;/li&gt;
&lt;li&gt;Understand how standalone runtime operates&lt;/li&gt;
&lt;li&gt;Explore CDSSO behaviour with P1AIC&lt;/li&gt;
&lt;li&gt;Observe complete end-to-end authentication flows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By simplifying setup and automating the foundational configuration, this approach accelerates learning and removes friction from experimentation. It serves as an ideal starting point for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers building familiarity with PingGateway&lt;/li&gt;
&lt;li&gt;Architects validating integration patterns&lt;/li&gt;
&lt;li&gt;Technical enablement sessions and workshops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most importantly, it provides a clear, structured baseline from which you can extend into more advanced routing logic, security hardening, certificate management, and production-ready deployment patterns.&lt;/p&gt;
&lt;p&gt;In short, this script turns what could be a multi-step manual configuration process into a streamlined, educational experience — helping you move from exploration to implementation with confidence.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CDSSO Documentation — &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/aic/cdsso.html&quot;&gt;https://docs.pingidentity.com/pinggateway/latest/aic/cdsso.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PingGateway Documentation — &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/latest/&quot;&gt;https://docs.pingidentity.com/pinggateway/latest/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CLIR6Db2_ungpY.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Ping Identity’s LTS, STS and Everything in Between: Because You Deserve Predictable Support</title><link>https://developer.pingidentity.com/blog/ping-identitys-lts-sts-everything-in-between-because-you-deserve-predictable-support/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/ping-identitys-lts-sts-everything-in-between-because-you-deserve-predictable-support/</guid><description>Learn how Ping Identity’s unified lifecycle policy uses Long Term Support and Short Term Support releases to make identity-platform upgrades more predictable.</description><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Ping Identity and ForgeRock merged in 2023, bringing the two identity companies together. ForgeRock products were subsequently renamed under the Ping Identity brand, although their support policies initially remained different.&lt;/p&gt;
&lt;p&gt;Ping Identity has announced a unified End of Life Policy based on Long Term Support (LTS). The unified policy takes effect in February 2026.&lt;/p&gt;
&lt;p&gt;This article explains the revised policy, its lifecycle stages, and examples of how enterprises can use the support timelines when planning identity-platform upgrades.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1: Ping Identity and ForgeRock — names and lifecycle-support evolution.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Versioning scheme&lt;/h2&gt;
&lt;p&gt;Ping Identity products use semantic versioning. A version contains three dot-separated components:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Major.Minor.Maintenance&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For example, the PingAM version 8.0.1 consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Major:&lt;/strong&gt; 8&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minor:&lt;/strong&gt; 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance:&lt;/strong&gt; 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a separate explanation of software-version numbering, see &lt;a href=&quot;https://manglubalasubramanian.medium.com/iupdate-iunderstand-making-sense-of-apples-and-the-software-industry-s-version-numbers-a37ebaeaa735&quot;&gt;Explanation of software version numbers&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Software product lifecycle stages&lt;/h2&gt;
&lt;p&gt;Ping Identity uses the following lifecycle designations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generally Available (GA)&lt;/strong&gt; — The product is available for purchase. The product release date is generally called the GA date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active Maintenance&lt;/strong&gt; — The product is actively developed, maintained, and supported.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End of Maintenance (EOM)&lt;/strong&gt; — Product maintenance has stopped, but customers may still receive support. Customers can submit support cases and obtain technical assistance under Ping’s support policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End of Support (EOS)&lt;/strong&gt; — The product is no longer maintained or supported.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End of Life (EOL)&lt;/strong&gt; — The product is obsolete and receives no maintenance, support, updates, or extended support.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 2: Key lifecycle stages.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Different software vendors may use different names for comparable lifecycle events. Check each vendor’s official definitions when assessing a product’s support status.&lt;/p&gt;
&lt;p&gt;Customers may purchase &lt;strong&gt;Extended Limited Support (ELS)&lt;/strong&gt; for an additional charge after a product reaches EOS. ELS is available only for specifically designated versions, so eligibility is not universal.&lt;/p&gt;
&lt;p&gt;Avoid operating products that are unsupported or have reached EOL. Regulated sectors, including financial services, may restrict the use of such software. For example, the UK Prudential Regulation Authority and Australia’s Australian Prudential Regulation Authority (APRA) provide guidance relevant to the use of unsupported software.&lt;/p&gt;
&lt;h2&gt;Current state lifecycle: from August 2023 through February 2026&lt;/h2&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Table 1: PingAM lifecycle-stage dates, based on the heritage ForgeRock support policy.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The table presents lifecycle dates for PingAM versions 7 and 8 under the earlier support arrangement. The key points are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Versions 7.0, 7.1, 7.2, and 7.3 have passed EOM but remain eligible for support.&lt;/li&gt;
&lt;li&gt;EOS, EOL, and EOM calculations use the major and minor version numbers.&lt;/li&gt;
&lt;li&gt;PingAM 7.5, the final minor version in the previous major series, does not yet have an EOM date.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Future state lifecycle and support: beyond February 2026&lt;/h2&gt;
&lt;p&gt;Beginning in February 2026, Ping Identity will designate product versions as either Long Term Support (LTS) or Short Term Support (STS).&lt;/p&gt;
&lt;p&gt;LTS releases are intended for critical deployments that prioritize operational consistency and longer security coverage. Banks and insurance companies are examples of organizations likely to favor this model.&lt;/p&gt;
&lt;p&gt;STS releases are intended for environments where obtaining new capabilities quickly is more important than having the longest support window.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Table 2: Comparison of LTS and STS versions.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Ping Identity expects, but does not guarantee, to introduce new LTS versions approximately every two years.&lt;/p&gt;
&lt;p&gt;The revised framework is intended to provide both LTS and STS support paths, with more predictable maintenance and upgrade planning.&lt;/p&gt;
&lt;h3&gt;Illustrative PingAM release sequence&lt;/h3&gt;
&lt;p&gt;The following hypothetical releases demonstrate how the lifecycle model could operate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AM 10 — STS:&lt;/strong&gt; March 2026&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 10.1 — STS:&lt;/strong&gt; May 2027&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 10.2 — LTS:&lt;/strong&gt; November 2027&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 11 — STS:&lt;/strong&gt; February 2028&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 11.1 — STS:&lt;/strong&gt; May 2028&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 11.2 — LTS:&lt;/strong&gt; August 2028&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Table 3: Illustrative lifecycle dates for a future PingAM release under the unified support policy.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Summary view of key benefits for enterprises&lt;/h2&gt;
&lt;p&gt;The unified policy is expected to provide four benefits for enterprises:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stability:&lt;/strong&gt; LTS releases provide three years of guaranteed support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictability:&lt;/strong&gt; A regular release pattern, with LTS versions expected roughly every two years, supports upgrade planning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility:&lt;/strong&gt; LTS versions may receive an optional additional year of ELS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity:&lt;/strong&gt; Heritage Ping and heritage ForgeRock products will follow one unified policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;From self-hosted to SaaS: are lifecycle stages and support still relevant, or just a legacy concern?&lt;/h2&gt;
&lt;p&gt;Many organizations are adopting software as a service (SaaS) and other hosted services.&lt;/p&gt;
&lt;p&gt;Ping Identity offers three cloud options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PingOne Advanced Services:&lt;/strong&gt; Single-tenant services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Advanced Identity Cloud:&lt;/strong&gt; Single-tenant services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PingOne Cloud Platform:&lt;/strong&gt; Multi-tenant services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these hosted offerings, customers do not generally manage product versions or lifecycle milestones themselves. Ping Identity manages platform updates, patches, and support, reducing the customer’s need to plan around individual software EOL dates.&lt;/p&gt;
&lt;h2&gt;Frequently asked questions&lt;/h2&gt;
&lt;h3&gt;Can STS versions be used in production?&lt;/h3&gt;
&lt;p&gt;Yes. Both STS and LTS releases are production-capable. The difference is that STS versions have shorter support periods, requiring customers to plan upgrades more frequently.&lt;/p&gt;
&lt;h3&gt;Do STS and LTS versions receive patches in the same way?&lt;/h3&gt;
&lt;p&gt;Both release types receive regular patches. However, Ping Identity may schedule the patches differently for STS and LTS versions.&lt;/p&gt;
&lt;h3&gt;Is there a fixed STS release schedule comparable to the approximate two-year LTS cadence?&lt;/h3&gt;
&lt;p&gt;No fixed STS timetable is specified. Each product family determines its STS timing according to its feature backlog and customer requirements.&lt;/p&gt;
&lt;p&gt;The stated goal is to provide at least one STS release per product each year, although some product families may publish more than one.&lt;/p&gt;
&lt;h3&gt;Does the new model extend the release approach already used by PingGateway?&lt;/h3&gt;
&lt;p&gt;Yes. PingGateway has used a similar LTS/STS-style approach since 2023, although it did not formally use those labels.&lt;/p&gt;
&lt;p&gt;The 2025.3, 2025.6, and 2025.9 releases are STS examples, and the forthcoming 2025.11 release is an LTS version. From February 2026, all Ping products are expected to use the LTS and STS framework.&lt;/p&gt;
&lt;p&gt;Under the new approach, the final minor release within each major release becomes the LTS version, following the practice PingGateway has used for its three-year support model.&lt;/p&gt;
&lt;h3&gt;Is there a current-version mapping for STS and LTS releases?&lt;/h3&gt;
&lt;p&gt;Yes. See Ping Identity’s &lt;a href=&quot;https://support.pingidentity.com/s/article/Ping-Identity-EOL-Tracker&quot;&gt;End of Life Tracker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Active versions designated as LTS or STS are covered by the unified policy. The example shown identifies PingAM 7.5 as LTS and PingAM 8.0 as STS. Other versions, including PingAM 7.4 and 7.3, continue under their existing maintenance and support policies.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Table 4: Active PingAM versions designated as STS and LTS.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Are whole major versions assigned as LTS or STS, as in the Java release model?&lt;/h3&gt;
&lt;p&gt;No. Ping Identity does not designate an entire major version as LTS or STS.&lt;/p&gt;
&lt;p&gt;Instead, the last minor release within each major version is the LTS release. Earlier minor releases in that same major series are STS. Consequently, each major release is expected to contain one LTS version and one or more STS versions.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Ping Identity is consolidating the lifecycle arrangements for its heritage Ping and ForgeRock products into a unified framework containing both LTS and STS release types.&lt;/p&gt;
&lt;p&gt;LTS releases are intended to provide a three-year guaranteed maintenance period, helping organizations operate critical systems with greater stability and plan future upgrades more consistently.&lt;/p&gt;
&lt;h2&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;The article thanks &lt;a href=&quot;https://www.linkedin.com/in/jessica-chow-15474169/&quot;&gt;Jessica Chow&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/in/matthew-berzinski-5772422/&quot;&gt;Matthew Berzinski&lt;/a&gt;, and &lt;a href=&quot;https://www.linkedin.com/in/marc-harris-5a569a3/&quot;&gt;Marc Harris&lt;/a&gt; for reviewing the article and providing suggestions.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://support.pingidentity.com/s/article/New-names-for-ForgeRock-products&quot;&gt;ForgeRock product renaming information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pingidentity.com/en/legal/end-of-life-policy.html&quot;&gt;Unified End of Life Policy, effective February 1, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://manglubalasubramanian.medium.com/iupdate-iunderstand-making-sense-of-apples-and-the-software-industry-s-version-numbers-a37ebaeaa735&quot;&gt;Explanation of Apple and software-industry version numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://semver.org/#semantic-versioning-200&quot;&gt;Semantic Versioning specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingone/p1_cloud__platform_main_landing_page.html&quot;&gt;PingOne documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/home.html&quot;&gt;PingOne Advanced Identity Cloud documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneadvancedservices/p1as_home.html&quot;&gt;PingOne Advanced Services documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pingoneadvancedservices/comparison_pingone_advanced_services_and_pingone_cloud_platform/p1as_cloud_comparison.html&quot;&gt;PingOne Advanced Services and PingOne Cloud Platform comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pingidentity.com/pinggateway/release-notes/timeline.html&quot;&gt;PingGateway release timeline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.pingidentity.com/s/article/Ping-Identity-EOL-Tracker&quot;&gt;Ping Identity EOL Software Tracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pingidentity.com/en/legal/support-policy.html&quot;&gt;Ping Identity Support Policy&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.Bc3_2NTG_u04cK.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Securing High-Risk Actions: Transactional Authorization over REST</title><link>https://developer.pingidentity.com/blog/securing-high-risk-actions-transactional-authorization-over-rest/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/securing-high-risk-actions-transactional-authorization-over-rest/</guid><description>A practical guide to securing high-risk actions with REST-based Transactional Authorization.</description><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@darinder.shokar/securing-high-risk-actions-transactional-authorization-over-rest-8e3a6fbf474b&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Requiring a user to explicitly authorise access to a sensitive resource every time they attempt to access it is a common security requirement. A typical example is in financial services, where initiating a payment might trigger a one-time passcode (OTP) sent to the user’s device, or a push notification requiring approval. These additional steps increase friction intentionally, ensuring the user is genuinely present and consenting before a high-risk action is allowed.&lt;/p&gt;
&lt;p&gt;In the PingOne Advanced Identity Cloud (P1AIC) and PingOne Advanced Identity Software (self-managed PingAM), this capability is known as Transactional Authorisation (TAuthZ). You can read more about it &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/am-authorization/transactional-authorization.html#understand_transactional_authorization&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;The most common and recommended architecture for enabling Transactional Authorisation is to use a Policy Enforcement Point (PEP) such as &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/2025.9/gateway-guide/stepup-sso-trx.html&quot;&gt;PingGateway&lt;/a&gt; or the &lt;a href=&quot;https://docs.pingidentity.com/web-agents/2025.9/index.html&quot;&gt;PingAgent&lt;/a&gt;, which interact with P1AIC or PingAM as the Policy Decision Point (PDP). Another pattern is to implement enforcement directly in applications via the &lt;a href=&quot;https://docs.pingidentity.com/sdks/latest/sdks/use-cases/how-to-perform-transactional-authorization.html&quot;&gt;Ping SDKs&lt;/a&gt; (JavaScript, iOS and Android).&lt;/p&gt;
&lt;p&gt;However, some customers who operate their own application gateways prefer to interact directly with the PDP REST APIs. This blog focuses on that third approach and provides a clear, end-to-end example using an automated shell script to demonstrate how the REST flow works.&lt;/p&gt;
&lt;p&gt;The high-level sequence of events for both application-based and gateway-based integrations is broadly the same. It’s explained in detail &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/am-authorization/transactional-authorization.html#understand_transactional_authorization&quot;&gt;here&lt;/a&gt;, but here’s the simplified flow:&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;Use Case&lt;/h2&gt;
&lt;p&gt;In this blog, we demonstrate a REST-based approach to Transactional Authorization (TAuthZ) using an automated shell script. The scenario covers a high-risk operation — accessing an example &lt;code&gt;makePayment&lt;/code&gt; API&lt;br /&gt;
(&lt;code&gt;https://api.bankingexample.com:443/makepayment&lt;/code&gt;) via an &lt;code&gt;HTTP POST&lt;/code&gt; request.&lt;/p&gt;
&lt;p&gt;The script performs the following steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authenticate the user using a standard login journey and obtain an SSO token&lt;/li&gt;
&lt;li&gt;Authenticate an admin using an admin-specific login journey in order to access the policy endpoint, issuing an SSO token with extended session durations (for example, a 60-minute idle and max time) so the client can cache it and improve performance.&lt;/li&gt;
&lt;li&gt;Calls the policy evaluation endpoint to request access to the protected &lt;code&gt;makePayment&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;Receive a policy advice with a &lt;code&gt;TransactionConditionAdvice&lt;/code&gt; (TxId) from the PDP instructing the client to execute a Transactional Authorization journey (for example, OTP or push approval).&lt;/li&gt;
&lt;li&gt;Executes the required TAuthZ journey with this &lt;code&gt;TxId&lt;/code&gt;, collecting and completing the necessary callbacks for the one-time step-up authentication flow.&lt;/li&gt;
&lt;li&gt;Resubmits the policy evaluation request, now including the completed transactional context and &lt;code&gt;TxId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Finally, receives a one-time authorization decision granting access to the sensitive API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This demonstrates how applications or gateways can interact directly with the PDP REST APIs to enforce secure, single-use authorization for high-risk operations.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Let’s get building 💪&lt;/p&gt;
&lt;p&gt;All assets are located &lt;a href=&quot;https://github.com/forgerock-dss/transactional_authorization/tree/main&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Import the Journeys&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For this example we’ll import three journeys:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NormalLogin&lt;/code&gt; — Used to simulate a standard user login.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AdminLogin&lt;/code&gt; — Used to simulate admin login with extended session timeouts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransactionalAuthZLogin&lt;/code&gt; — Used as the step-up authentication journey triggered by the TAuthZ policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Download the journey export from &lt;a href=&quot;https://github.com/forgerock-dss/transactional_authorization/blob/main/journey_exports/normal_admin_and_transactional_journey_export.json&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;From the P1AIC Platform UI, navigate to Journeys on the left panel &amp;gt; Click Import &amp;gt; Take a Backup if required &amp;gt; Select Browse &amp;gt; Locate and open the file from step 1 &amp;gt; Click Next &amp;gt; Start Import&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once the import is complete, the three new journeys will appear as shown below. Note that the &lt;code&gt;TransactionalAuthZLogin&lt;/code&gt; journey should be a strong MFA journey. However, a simplified version has been used here for clarity in this blog.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Import the Transactional Authorization Policy&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the policy XML file from &lt;a href=&quot;https://github.com/forgerock-dss/transactional_authorization/blob/main/policy_exports/Transactional_AuthZ_Policy_Export.xml&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;From the P1AIC Platform UI, expand Native Consoles &amp;gt; Click Access Management &amp;gt; Expand Authorization &amp;gt; Click Policy Sets.&lt;/li&gt;
&lt;li&gt;Click Import Policy Sets &amp;gt; Find the XML file from step 1 &amp;gt; Click Open&lt;/li&gt;
&lt;li&gt;Refresh the browser and the new &lt;code&gt;transactionalAuthZ&lt;/code&gt; Policy Set will appear.&lt;/li&gt;
&lt;li&gt;Clicking into it will show the &lt;code&gt;makePayment&lt;/code&gt; policy as shown below where:
&lt;ul&gt;
&lt;li&gt;The resource is defined as &lt;code&gt;https://api.bankingexample.com:443/makepayment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The allowed action is &lt;code&gt;HTTP POST.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The subject is set to &lt;code&gt;AuthenticatedUsers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The Transaction Environments condition targets the &lt;code&gt;TransactionalAuthZLogin&lt;/code&gt; journey.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create the Test Users and Apply Privileges&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;From the P1AIC Platform UI, Expand Identities &amp;gt; Click Manage &amp;gt; New Alpha realm — user &amp;gt; Create a user with the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Username: policy_user&lt;/li&gt;
&lt;li&gt;Firstname: Policy&lt;/li&gt;
&lt;li&gt;Lastname: User&lt;/li&gt;
&lt;li&gt;Email Address: &lt;a href=&quot;mailto:policy_user@example.com&quot;&gt;policy_user@example.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;!!_SeCu4E@Pa55w04D$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Repeat Step 1 but this time for the policy admin user:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Username: policy_admin&lt;/li&gt;
&lt;li&gt;Firstname: Policy&lt;/li&gt;
&lt;li&gt;Lastname: Admin&lt;/li&gt;
&lt;li&gt;Email Address: &lt;a href=&quot;mailto:policy_admin@example.com&quot;&gt;policy_admin@example.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;!!_Adm1nSeCu4E@Pa55w04D$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;From the Access Management Native Console, navigate to Identities &amp;gt; Groups tab &amp;gt; Add Group &amp;gt; Set the Group ID to &lt;code&gt;policy_admin&lt;/code&gt; &amp;gt; Create.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add the &lt;code&gt;policy_admin&lt;/code&gt; user as a member and hit Save.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;From the Privileges tab, enable &lt;code&gt;Entitlement Rest Access&lt;/code&gt; and hit Save Changes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Execute the Script&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now that we have all the configuration in place, let’s bring it to life with the script.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the script from &lt;a href=&quot;https://github.com/forgerock-dss/transactional_authorization/blob/main/scripts/transactional_authZ.sh&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Modify the AM_URL to match your environment&lt;/li&gt;
&lt;li&gt;Execute the script to demonstrate the flow&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The output will look this like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;./transactional_authZ_DS.sh  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Getting cookie name  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;CookieName is: XXXX&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Creating end-user SSO token for user: policy_user  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;End-user SSO token is: ZER7HY8OL...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Creating policy-admin SSO token for user: policy_admin  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Policy-admin SSO Token is: o0jzAU....&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Calling transactional policy: transactionalAuthZ  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Transaction Condition Advice Id is: 195b8505-3dba-4bbc-a71a-7debed2c2596&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Calling ../authenticate endpoint with Transaction Condition Advice Id: 195b8505-3dba-4bbc-a71a-7debed2c2596 to get callbacks&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Completing callbacks for submission  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Completed callback payload:  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;authId&quot;: &quot;eyJ0eXAiOiJKV1QiLC....74&quot;,  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;callbacks&quot;: [  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;....&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Calling authenticate endpoint with advice and completed callbacks  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;tokenId&quot;: &quot;ZER7HY8O....IwMQ..*&quot;,  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;successUrl&quot;: &quot;/enduser/?realm=/alpha&quot;,  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;realm&quot;: &quot;/alpha&quot;  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Finally, calling policy endpoint post transactional AuthZ with Transactional Conditional Advice Id of: 195b8505-3dba-4bbc-a71a-7debed2c2596  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;resource&quot;: &quot;https://api.bankingexample.com:443/makepayment&quot;,  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;actions&quot;: {  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;POST&quot;: true  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;},  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;attributes&quot;: {},  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;advices&quot;: {},  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;ttl&quot;: 0  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}  &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;*********************&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Transactional Authorization (TAuthZ) is a powerful capability within PingOne Advanced Identity Cloud (P1AIC) and PingAM, enabling organisations to enforce strong, step-up authentication precisely at the moments where risk is highest. Whether approving a financial transaction, confirming a high-value action, or validating a user’s intent, TAuthZ offers a reliable way to introduce friction only when it truly matters.&lt;/p&gt;
&lt;p&gt;This blog illustrates how to execute a complete Transactional Authorization flow entirely over REST. By walking through user authentication, admin token retrieval, policy evaluation, TxId handling, step-up journey execution, and final policy re-evaluation, we showed how an application or custom gateway can interact directly with the PDP — while still leveraging the full power and flexibility of PingAM’s decisioning capabilities.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.DMaHsK8J_Z7WL5D.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Smarter “Remember Me” with PingOne Protect</title><link>https://developer.pingidentity.com/blog/smarter-remember-me-with-pingone-protect/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/smarter-remember-me-with-pingone-protect/</guid><description>Smarter “Remember Me” with PingOne Protect: seamless, secure logins that adapt to risk without disrupting the user experience.</description><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://medium.com/@darinder.shokar/smarter-remember-me-with-pingone-protect-f71a54d7fd80&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In the world of Identity and Access Management (IAM), it’s always a balancing act between security and user experience. Organisations want to introduce just the right amount of friction, at the right moment, and in the right context to protect users without disrupting them. The goal is to maintain a seamless, trusted customer journey while minimising drop-off and frustration.&lt;/p&gt;
&lt;p&gt;To achieve this, many businesses have implemented a “Remember Me” capability within their web channels. Once a user successfully signs in, subsequent visits allow them to be logged in automatically without re-entering their credentials — a familiar and convenient feature which is especially common in retail environments.&lt;/p&gt;
&lt;p&gt;However, “Remember Me” in isolation has its flaws. While it delivers convenience, it often lacks the intelligence to adapt when the context changes. For example, what if a user originally authenticates from the UK but suddenly appears to log in from Singapore — an &lt;em&gt;impossible traveller&lt;/em&gt; scenario? What if their IP address shifts unexpectedly, the login occurs at an unusual time, or even worse, a bot attempts access? Without continuously evaluating these contextual signals, “Remember Me” might meet the bar for user experience but falls short on adaptive security — failing to introduce the right friction at the right moment.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;This is where &lt;a href=&quot;https://www.pingidentity.com/en/product/pingone-protect.html&quot;&gt;PingOne Protect&lt;/a&gt; steps in. It continuously analyses behavioural and contextual signals across multiple attack vectors to assign risk scores and trigger appropriate mitigation or action, to ensure convenience is not achieved at the expense of security.&lt;/p&gt;
&lt;p&gt;To learn more about the use cases for PingOne Protect integration with PingOne Advanced Identity Cloud (P1AIC) check out &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/integrations/pingone-protect-use-cases.html&quot;&gt;this&lt;/a&gt; link.&lt;/p&gt;
&lt;h2&gt;Use Case&lt;/h2&gt;
&lt;p&gt;In this blog, we’ll enhance the &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/persistent-cookie-decision.html&quot;&gt;Persistent Cookie&lt;/a&gt; feature in PingOne Advanced Identity Cloud — the functionality that enables the “Remember Me” capability by integrating it with PingOne Protect.&lt;/p&gt;
&lt;p&gt;Here’s how it works:&lt;/p&gt;
&lt;p&gt;During the initial login, PingOne Protect evaluates the context and returns a risk score:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the score is low, a Persistent Cookie (PCookie) is set, and the login proceeds as normal.&lt;/li&gt;
&lt;li&gt;If the score is not low, no PCookie is set — effectively disabling “Remember Me” for that session, but does so silently, without disrupting the user experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On subsequent visits, if a valid PCookie is found, PingOne Protect performs another risk assessment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the risk remains low, the cookie is trusted, a new session is created automatically and the user is seamlessly signed in.&lt;/li&gt;
&lt;li&gt;If the risk is elevated, the user isn’t blocked outright; instead, the PCookie is deleted and they’re redirected to re-enter their credentials — introducing just the right level of friction, at the right moment, based on the current context and signals.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Let’s get building! 💪&lt;/p&gt;
&lt;p&gt;Before we dive in, it’s worth noting that this solution showcases the power of the PingOne platform — apart from a single Custom Node used to display nodeState variables for clarity and education, it’s built entirely with out-of-the-box nodes, requiring no customisation or extensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setup a Mapped PingOne Environment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this section we’ll create a PingOne environment with PingOne Protect deployed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If you have a PingOne subscription navigate to &lt;a href=&quot;https://www.pingidentity.com/&quot;&gt;this&lt;/a&gt; page and hit “Sign On” at the top right of the page and login. On success you’ll be re-directed to the &lt;a href=&quot;https://console.pingone.eu/&quot;&gt;https://console.pingone.eu/&lt;/a&gt; page. If you don’t have a subscription, you can get a demo environment through this &lt;a href=&quot;https://www.pingidentity.com/en/try-ping.html&quot;&gt;link&lt;/a&gt;. Enter your business email address and hit “Try Ping”.&lt;/li&gt;
&lt;li&gt;From the PingOne Console &amp;gt; Hit Environments on the left panel &amp;gt; Blue + icon next to Environments.&lt;/li&gt;
&lt;li&gt;Select Build your own solution &amp;gt; click PingOne Protect &amp;gt; Hit Next.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On the environment name enter an appropriate name, for example &lt;code&gt;env-pingoneaic-**mycompany-ew2-sandbox1**&lt;/code&gt; &amp;gt; Select the region &amp;gt; Hit Finish.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Create a Worker Application&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Follow &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/integrations/pingone-set-up-workers.html#create-a-worker-application-in-each-mapped-pingone-environment&quot;&gt;these&lt;/a&gt; steps to create OIDC credentials for the PingOne AIC tenant to integrate with PingOne Protect. Note the PingOne API and Authorization URLs, for example &lt;a href=&quot;https://auth.pingone.eu/&quot;&gt;https://auth.pingone.&lt;/a&gt;com, &lt;a href=&quot;https://auth.pingone.eu/&quot;&gt;https://auth.pingone.eu&lt;/a&gt; etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create Environment Secrets and Variables (ESVs) in P1AIC&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Follow &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/integrations/pingone-set-up-workers.html#create-esvs-for-the-worker-application-credentials-in-each-tenant-environment&quot;&gt;these&lt;/a&gt; steps to create three ESVs that map to the PingOne Worker Credentials from the last step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Additionally, create an ESV named &lt;code&gt;esv-hmac-signing-key&lt;/code&gt; (string secret) to sign the persistent cookie. Generate a 256-bit key using one of the following commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL: &lt;code&gt;openssl rand -base64 32&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally create an ESV (string variable) called &lt;code&gt;esv-persist-cookie-domain&lt;/code&gt;with a value of the Fully Qualified Domain Name of your tenant. For example &lt;code&gt;openam-darinder-test.forgerock.io&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These ESVs make it easy to update configuration values when promoting to higher environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a PingOne Service in P1AIC&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Follow &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/latest/integrations/pingone-set-up-workers.html#create-a-service-in-your-development-environment&quot;&gt;these&lt;/a&gt; steps to create a PingOne Service in your P1AIC tenant. For the secondary configuration be sure to use the ESVs defined above.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;After creating the configuration set the PingOne API Server URL and Authorization Server URL as per the address noted in the “Create a Worker Application” section:
&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Import the Custom Display NodeState Node&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download &lt;code&gt;Display-NodeState.json&lt;/code&gt; from GitHub &lt;a href=&quot;https://github.com/forgerock-dss/remember_me_pingone_protect/tree/main/custom_nodes&quot;&gt;here&lt;/a&gt; to your local machine.&lt;/li&gt;
&lt;li&gt;From the P1AIC platform admin UI, expand Journeys on the left navigation panel &amp;gt; Custom Nodes&lt;/li&gt;
&lt;li&gt;Click Import Nodes (or Import if other Custom Nodes are present) &amp;gt; Browse &amp;gt; open &lt;code&gt;Display-NodeState.json&lt;/code&gt; &amp;gt; Import Nodes &amp;gt; Done. A new node called Output Variable from NodeState should be created.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Journey Import&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;From the &lt;code&gt;journey_exports&lt;/code&gt; directory &lt;a href=&quot;https://github.com/forgerock-dss/remember_me_pingone_protect/tree/main/journey_exports&quot;&gt;here&lt;/a&gt;, download the journey to your local machine.&lt;/li&gt;
&lt;li&gt;From the P1AIC platform admin UI, expand Journeys on the left navigation panel &amp;gt; click Journeys &amp;gt; Import.&lt;/li&gt;
&lt;li&gt;If need be take a backup or skip.&lt;/li&gt;
&lt;li&gt;Click Browse &amp;gt; find the &lt;code&gt;PersistWithProtect-journeyExport.json&lt;/code&gt; file &amp;gt; Open &amp;gt; Next &amp;gt; Start Import.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;p&gt;A few points to note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The PingOne Protect Evaluation nodes uses the default &lt;code&gt;Risk Policy Set ID.&lt;/code&gt; As a best practice a new risk policy should be created and the ID set in this parameter. From the PingOne Console &amp;gt; Expand Threat Protection &amp;gt; Risk Policies &amp;gt; Global Policies Blue + &amp;gt; Set a name &amp;gt; Configure as required &amp;gt; Apply &amp;gt; Copy the resulting Policy ID into this parameter. For example &lt;code&gt;a05cccb8-ada5–44e3-a6cd-cefdbc0c942f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Additional &lt;code&gt;BOT_MITIGATION&lt;/code&gt; and &lt;code&gt;AITM_MITIGATION&lt;/code&gt; (Adversary-In-The-Middle) &lt;a href=&quot;https://docs.pingidentity.com/auth-node-ref/latest/pingone/pingone-protect-evaluation.html&quot;&gt;items&lt;/a&gt; were added to the Recommended Actions parameter of the PingOne Protect Evaluation nodes to demonstrate the node’s flexibility in mapping specific risk results to additional outcomes.&lt;/li&gt;
&lt;li&gt;The PingOne Protect Result node captures additional context just before the session is created, ensuring maximum accuracy for future assessment of the user’s risk posture and behaviour.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Journey Breakdown&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;This journey demonstrates how to implement a “Remember Me” (persistent login) flow enhanced with PingOne Protect risk evaluation.&lt;br /&gt;
It combines persistent cookie management with adaptive risk analysis to allow seamless logins for low-risk sessions while enforcing re-authentication for higher-risk ones.&lt;/p&gt;
&lt;p&gt;On every execution of the journey, the PingOne Protect Initialize node runs first to begin context and device data capture.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The journey then checks for the presence of a persistent cookie named &lt;code&gt;persist-session&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If persist-session is found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The PingOne Protect Evaluation node calls PingOne Protect to evaluate the context and calculate risk.&lt;/li&gt;
&lt;li&gt;For a Low risk outcome, the PingOne Protect Result node is invoked, and seamless “Remember Me” functionality is preserved.&lt;/li&gt;
&lt;li&gt;For any other outcome, the persistent cookie is removed, the PingOne Protect Risk value stored in nodeState is displayed for insight, and the user is redirected to authenticate again.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If persist-session is not found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The user is prompted for their username and password, and the PingOne Protect Evaluation node again evaluates context and risk.&lt;/li&gt;
&lt;li&gt;If a Low risk outcome is returned, a new persistent cookie is set, enabling “Remember Me” on future logins.&lt;/li&gt;
&lt;li&gt;For all other outcomes (except ClientError, which shows an error message), the PingOne Protect Risk value is displayed for eudcation and understanding.&lt;/li&gt;
&lt;li&gt;Login is still permitted — the session proceeds, but without setting the persistent cookie — ensuring user access is not disrupted, only “Remember Me” functionality is withheld.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;Check out the video below for a live demo&lt;/p&gt;

&lt;p&gt;PingOne Protect offers powerful monitoring and visual &lt;a href=&quot;https://docs.pingidentity.com/pingone/threat_protection_using_pingone_protect/p1_protect_dashboard.html&quot;&gt;dashboards&lt;/a&gt; that translate complex security data into clear, actionable insights. With real-time visibility into analysed events, risk trends, geolocation anomalies and high-risk user behaviour, organisations can quickly assess and respond to emerging threats. Here’s an example dashboard:
&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The integration of PingOne Protect with PingOne Advanced Identity Cloud elevates a simple “Remember Me” feature into an intelligent, adaptive security capability. By continuously evaluating contextual and behavioural risk, it ensures that convenience does not come at the expense of protection.&lt;/p&gt;
&lt;p&gt;Users enjoy seamless, low-friction logins when their behaviour is trusted, while higher-risk sessions automatically trigger the right level of friction — discreetly and without disruption. The result is a smarter, more secure “Remember Me” experience that balances usability with robust risk-based control — all achieved through a no-code, out-of-the-box Ping Identity platform implementation.&lt;/p&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CTi6Rdvk_Z15JE7d.webp" medium="image" type="image/webp" width="1480" height="740"/></item><item><title>Unleashing Success: Key Ingredients for PingOne Advanced Identity Cloud</title><link>https://developer.pingidentity.com/blog/unleashing-success-key-ingredients-for-aic/</link><guid isPermaLink="true">https://developer.pingidentity.com/blog/unleashing-success-key-ingredients-for-aic/</guid><description>Explore the planning, architecture, integration, testing, and change-management practices that help identity teams succeed with PingOne Advanced Identity Cloud.</description><pubDate>Mon, 05 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href=&quot;https://marknienaber.medium.com/unleashing-success-key-ingredients-for-a-forgerock-identity-cloud-project-b1fc501b145d&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In today’s digital landscape, organizations face a multitude of challenges when it comes to managing identities and ensuring secure access to their systems. &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/home.html&quot;&gt;PingOne Advanced Identity Cloud&lt;/a&gt; is a powerful solution that enables businesses to streamline their identity and access management (IAM) processes. However, the success of a PingOne Advanced Identity Cloud project depends on several critical factors. In this blog post, we will explore the key ingredients necessary for a successful PingOne Advanced Identity Cloud project.&lt;/p&gt;
&lt;h2&gt;Roles and responsibility&lt;/h2&gt;
&lt;p&gt;Understanding the roles and responsibility of the PingOne Advanced Identity Cloud components, external applications, and overall solution will allow an organisation to allocate resources during the project and help maintain the solution moving forward. Knowing where to find the right resources to assist will expedite issue resolution and allow for more efficient solution development.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h3&gt;Platform components&lt;/h3&gt;
&lt;p&gt;PingOne Advanced Identity Cloud is a SaaS application deployed to Google Cloud Platform. The software components deployed inside the platform are managed by Ping Identity, while external software components that make up the overall solution but do not fit inside the platform are managed by the organisation.&lt;/p&gt;
&lt;h3&gt;Ping Identity components&lt;/h3&gt;
&lt;p&gt;Ping Identity is responsible for deploying, managing, upgrading, and monitoring the various software components as part of the platform. Ping Identity also regularly backs up data and provides &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/tenants/environments-architecture-availability-disaster-recovery.html&quot;&gt;High Availability and Disaster Recovery&lt;/a&gt;. Organisations no longer have to be concerned with spinning up complex architectures for various environments, as Ping Identity does this all for you. Each customer receives a development, staging, and production environment to start, and customers can request any number of sandbox environments to support development; see &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/tenants/environments-development-staging-production.html&quot;&gt;the environment documentation&lt;/a&gt; for more detail.&lt;/p&gt;
&lt;p&gt;The following Ping Identity software and services are all wrapped up in a single PingOne Advanced Identity Cloud Platform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PingAM&lt;/li&gt;
&lt;li&gt;PingIDM&lt;/li&gt;
&lt;li&gt;PingDS&lt;/li&gt;
&lt;li&gt;Cloud DevOps — &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/tenants/environments-development-staging-production.html#promoting_configuration&quot;&gt;Promoting configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Admin UIs / Hosted Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Organisation components&lt;/h3&gt;
&lt;p&gt;The organisation is responsible for deploying, managing, and upgrading the software and solution components deployed outside of PingOne Advanced Identity Cloud, but still acting as part of the overall solution. Of course, any Ping Identity products deployed onsite will still be covered by Ping Identity product support.&lt;/p&gt;
&lt;p&gt;The organisation’s components may include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom UIs backed by the &lt;a href=&quot;https://docs.pingidentity.com/pingoneaic/end-user/sdks.html&quot;&gt;Ping SDKs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Remote Connector Servers&lt;/li&gt;
&lt;li&gt;Edge components like &lt;a href=&quot;https://docs.pingidentity.com/pinggateway/2026/index.html&quot;&gt;PingGateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The organisation’s own CI/CD pipeline&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marknienaber.medium.com/unleashing-success-key-ingredients-for-a-forgerock-identity-cloud-project-b1fc501b145d#4ffa&quot;&gt;Overall solution monitoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Customer solution&lt;/h3&gt;
&lt;p&gt;During the project, a solution will be custom-built by the deployment team to meet the organisation’s requirements. This solution is the product that the organisation owns and is delivering to its customers. Maintenance, monitoring, and enhancements to this product are the responsibility of the organisation. Ping Identity maintains responsibility for the underlying software inside PingOne Advanced Identity Cloud and offers around-the-clock support for any issues that may arise with the software.&lt;/p&gt;
&lt;p&gt;For organisations to support the product they are delivering to their customers, some may train or even hire internal technical resources, consult a partner organisation or SI, and may engage Ping Identity technical consultants as required. See &lt;a href=&quot;https://www.pingidentity.com/en/support/professional-services.html&quot;&gt;Ping Identity technical consulting services&lt;/a&gt; for more information.&lt;/p&gt;
&lt;h2&gt;Technical skills and training&lt;/h2&gt;
&lt;p&gt;Ping Identity provides comprehensive product training for all of its products; these can be accessed through &lt;a href=&quot;https://training.pingidentity.com/&quot;&gt;Ping Identity Training&lt;/a&gt;. It is highly recommended that technical resources assigned to the PingOne Advanced Identity Cloud project have attended relevant courses. The courses can help enable resources prior to the project and on an ongoing basis as new technical resources are onboarded. Working with a trusted partner or SI can also help to reinforce the technical skills required. Ping Identity technical consultants will work alongside your teams and assist with the implementation.&lt;/p&gt;
&lt;h2&gt;Clear objectives and requirements&lt;/h2&gt;
&lt;p&gt;The foundation of any successful project lies in clearly defining its objectives and requirements. Start by identifying the pain points within your organisation’s current identity management infrastructure and determine the specific goals you aim to achieve with the PingOne Advanced Identity Cloud. These objectives could include enhancing user experience, strengthening security measures, or streamlining administrative tasks.&lt;/p&gt;
&lt;p&gt;When moving your solution from an on-premise or legacy system to PingOne Advanced Identity Cloud, we recommend you “Reimagine” your solution using this opportunity to retire technical debt. What are the outcomes that need to be achieved, how will successful achievement of those outcomes be measured, and how will those outcomes benefit your organisation and its customers? Building this into a well-defined roadmap allows alignment between the project team and stakeholders around a shared vision. It is critical that this vision is defined before the project commences, coupled with a clearly defined MVP and key outcome milestones with measurable success criteria.&lt;/p&gt;
&lt;p&gt;If you can’t define &lt;em&gt;success&lt;/em&gt;, you can’t succeed.&lt;/p&gt;
&lt;h2&gt;Comprehensive planning and strategy&lt;/h2&gt;
&lt;p&gt;Once the project objectives are established, it’s essential to develop a comprehensive plan and strategy. Consider factors such as the scope of the project, the resources required, the timeline, and the potential risks involved. Collaborate closely with key stakeholders, including IT teams, security experts, and business leaders, to ensure that the project aligns with broader organisational goals and addresses specific user needs effectively. A project manager for the organisation sitting as part of the project team, and with clear boundaries and scope, will ensure the project focuses on its MVP while meeting its broader long-term goals.&lt;/p&gt;
&lt;h2&gt;Robust architecture and design&lt;/h2&gt;
&lt;p&gt;The architecture and design of your PingOne Advanced Identity Cloud deployment are critical for its success. Work closely with experienced architects and developers to design a scalable, flexible, and secure solution that can accommodate your organisation’s current and future requirements. Consider factors such as user volumes, integration capabilities, compliance regulations, and the need for customisation. A well-thought-out architecture will help optimise performance, minimise downtime, and support future expansion. Ping Identity technical consultants will work with the organisation to ensure the solution architecture developed is sufficient for your needs and meets best practices, and can then be incorporated into the organisation’s overall design.&lt;/p&gt;
&lt;h2&gt;Seamless integration and data migration&lt;/h2&gt;
&lt;p&gt;A successful implementation of PingOne Advanced Identity Cloud relies on seamless integration with your existing systems. Ensure that your project team has a deep understanding of your organisation’s technology landscape to establish smooth integration points. A careful approach to data migration is also vital to ensure a seamless transition from legacy systems to the new solution. Data cleansing, mapping, and validation processes should be executed meticulously to maintain data integrity and prevent any disruptions to user access or experience. Many organisations’ legacy systems retain inaccurate or even corrupt data; it’s critical that the migration to a cutting-edge platform like PingOne Advanced Identity Cloud is viewed as an opportunity to remove this technical debt. Ping Identity technical consultants can help the organisation understand what is necessary to meet requirements.&lt;/p&gt;
&lt;h2&gt;Thorough testing and quality assurance&lt;/h2&gt;
&lt;p&gt;Testing and quality assurance are crucial stages of any project implementation. Conduct comprehensive testing to validate the functionality, security, and performance of your PingOne Advanced Identity Cloud deployment. Test different scenarios, such as user registration, authentication, authorisation, and password management, to ensure a seamless end-user experience. Invest in automated testing tools and techniques to increase efficiency and accuracy while minimising the risk of human error. Work with your Ping Identity customer deployment manager and Ping Identity technical consultants on planning your testing to ensure your solution is ready for your customers.&lt;/p&gt;
&lt;h2&gt;User adoption and change management&lt;/h2&gt;
&lt;p&gt;Successful implementation of PingOne Advanced Identity Cloud is not just about the technical aspects; it also requires a focus on user adoption and change management. Plan and execute a robust change management strategy that includes clear communication, training programs, and ongoing support to ensure a smooth transition for end users. Address any concerns or resistance to change by highlighting the benefits of the new system and demonstrating its usability. This is particularly important as the move to PingOne Advanced Identity Cloud offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modern standards-based integration patterns&lt;/li&gt;
&lt;li&gt;Opportunities to provide dynamic yet secure authentication options&lt;/li&gt;
&lt;li&gt;The ability to remove antiquated legacy functionality as you move into the future&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;h2&gt;Continuous monitoring and optimization&lt;/h2&gt;
&lt;p&gt;Once your PingOne Advanced Identity Cloud project is live, it’s crucial to have a process for continuous monitoring and optimization.&lt;/p&gt;
&lt;p&gt;Implement proactive monitoring tools to identify and address potential service disruptions and maximise customer satisfaction. For example, monitoring authentication may not provide a complete picture of the entire user experience. It may be important to include self-service flows, authentication, authorization, token generation, and other critical operations. Monitoring the correct critical aspects of your solution will enable the business to proactively react.&lt;/p&gt;
&lt;p&gt;Regularly review and optimize the solution based on user feedback, evolving business requirements, and industry best practices. Stay up to date with Ping Identity’s releases and updates to leverage new features and enhancements that can further improve your IAM processes. Ping Identity Support can be contacted around the clock to provide product support, and Ping Identity technical consultants or one of Ping Identity’s experienced partners can be engaged to assist with the solution.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;A successful PingOne Advanced Identity Cloud project requires a combination of careful planning, robust architecture, seamless integration, thorough testing, user-centric change management, and ongoing monitoring and optimization. A clear understanding of the roles and responsibilities of the platform and solution will help with resource allocation and expedite issue resolution.&lt;/p&gt;
&lt;p&gt;By investing time and effort into each of these key ingredients, organizations can unlock the full potential of Ping Identity’s powerful IAM solution, delivering enhanced security, improved user experience, and streamlined identity management processes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do you have thoughts or questions on this article? Join the discussion on the &lt;a href=&quot;https://support.pingidentity.com/s/topic/0TO1W000000Q9oRWAS/developer&quot;&gt;Ping Identity developer community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><media:content url="https://developer.pingidentity.com/blog/_astro/title-image.CXn8SIqC_Zyum83.webp" medium="image" type="image/webp" width="1480" height="740"/></item></channel></rss>