<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DarkEdges</title>
    <description>The latest articles on DEV Community by DarkEdges (@darkedges).</description>
    <link>https://dev.to/darkedges</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1396307%2F1b5a1a60-77b1-40cd-89e3-0cfb704caf5a.jpeg</url>
      <title>DEV Community: DarkEdges</title>
      <link>https://dev.to/darkedges</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darkedges"/>
    <language>en</language>
    <item>
      <title>Custom ID-JAG on PingFederate, Part 4: Testing Live Issuance Without Overclaiming</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:54:58 +0000</pubDate>
      <link>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-4-testing-live-issuance-without-overclaiming-2cbl</link>
      <guid>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-4-testing-live-issuance-without-overclaiming-2cbl</guid>
      <description>&lt;p&gt;A successful token response is useful evidence. It is not the whole test.&lt;/p&gt;

&lt;p&gt;For this custom PingFederate ID-JAG implementation, we wanted to know whether the server would reject the requests it should reject, whether the assertion could be verified independently, and whether Terraform still matched the running configuration afterward.&lt;/p&gt;

&lt;p&gt;The live runner passed 27 checks on a server reporting version 12.3.3.1. This final part explains what those checks mean and what they leave open.&lt;/p&gt;

&lt;p&gt;Code for this series: &lt;a href="https://github.com/darkedges/pf12.3-id-jag-poc" rel="noopener noreferrer"&gt;project repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four kinds of evidence, kept separate
&lt;/h2&gt;

&lt;p&gt;The project has four test layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;26 Java offline checks, including signing and selected real PF parser, selector and serializer classes.&lt;/li&gt;
&lt;li&gt;24 Node tests covering client protocol behavior and verification with local RSA fixtures.&lt;/li&gt;
&lt;li&gt;Four Terraform tests using mocked providers to check configuration restrictions.&lt;/li&gt;
&lt;li&gt;27 live HTTP checks against the running PingFederate token and JWKS endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Java tests use the real 12.3.3 libraries but do not start an HTTP server. Some selected internal behavior is exercised reflectively in test code only. The deployed plugin does not use reflection.&lt;/p&gt;

&lt;p&gt;The Node client tests include mocked responses. The Terraform tests do not configure a real server.&lt;/p&gt;

&lt;p&gt;Only the live runner establishes that the installed plugin, configured policies, client authentication, request mappings and managed signing keys worked together on the running server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the issuing-side lab
&lt;/h2&gt;

&lt;p&gt;The repository contains the complete implementation; the following commands assume you are running from its root.&lt;/p&gt;

&lt;p&gt;First, build with a local PF 12.3.3 installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\generator\build.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PfInstallDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\path\to\pingfederate\12.3.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--test&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build script runs the standalone Java proof suite. Maven packaging is also supported, but Maven does not execute that standalone suite automatically.&lt;/p&gt;

&lt;p&gt;Deploy the resulting JAR to the test server's &lt;code&gt;server/default/deploy&lt;/code&gt; directory and restart it. Wait for the Admin API to become ready and confirm that the custom generator descriptor appears.&lt;/p&gt;

&lt;p&gt;Next, create the local subject fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;scripts/create-local-fixture.mjs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a disposable subject-signing key and two independent client secrets under the ignored &lt;code&gt;.local/&lt;/code&gt; directory. Repeat runs preserve existing keys and secrets.&lt;/p&gt;

&lt;p&gt;The subject issuer is a reserved example-domain identifier. Its public JWKS is explicitly configured in the JWT processor. This is a controlled test fixture, not an identity provider completing a real login.&lt;/p&gt;

&lt;p&gt;Follow the Terraform guide to supply administrator credentials, initialize the providers, inspect a plan and apply it. Do not copy administrator passwords into source files or published commands.&lt;/p&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;scripts/live-local-proof.mjs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner is deliberately restricted to the local runtime endpoint. It does not send fixture credentials to a downstream example domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the assertion, not just the HTTP status
&lt;/h2&gt;

&lt;p&gt;The positive exchange checks that the response uses the ID-JAG token-type URI and &lt;code&gt;token_type&lt;/code&gt; equals &lt;code&gt;N_A&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The runner then fetches the configured PF JWKS and verifies the compact JWS signature. It checks the expected signing algorithm, JWT type, key identifier, issuer, audience, client, resource, scope and lifetime, along with subject and replay-identifier presence.&lt;/p&gt;

&lt;p&gt;An additional check confirms that the issued subject is the authorized fixture subject. The response expiry must be positive and no greater than 300 seconds.&lt;/p&gt;

&lt;p&gt;These checks distinguish a parseable token from an assertion whose signature and intended bindings have actually been verified.&lt;/p&gt;

&lt;p&gt;An example JWT is returned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Issued&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ID-JAG&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oauth-id-jag+jwt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IhqVmmqnFs0necq8KJgwg-W_8UY_RS256"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:9031"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xaa-test-user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://target.example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"target-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.test/reports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read:reports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788766854&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788767154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"jti"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1847e9eb-b727-4372-aade-bd6b6bc10704"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Signature:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;present&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;displayed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exercise the rejection paths
&lt;/h2&gt;

&lt;p&gt;The live suite sends subject tokens with the wrong issuer, wrong audience, wrong authorized party and unauthorized subject. It also tests an absent authorized party, expired tokens, missing expiry, multiple subject audiences and a modified signature.&lt;/p&gt;

&lt;p&gt;At the request layer, it tests missing and duplicate audience, resource and scope parameters, as well as unauthorized destinations, scopes and requested token types.&lt;/p&gt;

&lt;p&gt;At the client-authentication layer, it tests missing credentials, an incorrect secret and an authentication attempt using the disabled compatibility client.&lt;/p&gt;

&lt;p&gt;All of those cases were rejected. The runner requires an appropriate error status and OAuth error response without an issued access token; it does not claim that every rejection came from the custom generator. Several should fail earlier in PingFederate's pipeline.&lt;/p&gt;

&lt;p&gt;No subject token, issued assertion or client secret is printed by the runner. It reports check names and a summary. Keep server-side diagnostics protected too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recheck the managed configuration
&lt;/h2&gt;

&lt;p&gt;After the live run, Terraform refreshed the eight managed resources and reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No changes. Your infrastructure matches the configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detailed exit code was zero.&lt;/p&gt;

&lt;p&gt;That establishes a clean post-apply plan for this configuration. It does not establish import behavior, every possible drift-recovery case, or a tested disaster-recovery process.&lt;/p&gt;

&lt;p&gt;Preserve the state and fixture inputs for the lab. Losing state and applying again to the same server is not a safe substitute for importing the existing resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  The work that remains
&lt;/h2&gt;

&lt;p&gt;This proof stops at issuing and independently verifying an ID-JAG.&lt;/p&gt;

&lt;p&gt;Real OIDC login must still be connected to the intended issuer. Subject resolution and authorization need production policy rather than one allowed fixture user.&lt;/p&gt;

&lt;p&gt;The downstream authorization server must be configured to trust the issuer and enforce the agreed assertion profile, authenticated client binding, resource and scope authorization, and replay handling. It must actually issue an access token before the full application-to-API path can be claimed as working.&lt;/p&gt;

&lt;p&gt;Multi-node deployment, key rotation behavior and broader profile conformance also need testing. The target URLs in this lab are examples, not deployed services.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful result with a clear boundary
&lt;/h2&gt;

&lt;p&gt;The result is specific and reproducible: a custom generator on the tested PingFederate 12.3.3.1 server issued an ID-JAG using PF-managed keys, its signature verified against the live JWKS, 27 HTTP checks passed, and Terraform's post-apply plan was clean.&lt;/p&gt;

&lt;p&gt;That is enough to establish the issuing-side extension path as viable. It is not a reason to skip the remaining trust, authorization and receiving-side work.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>oauth</category>
      <category>security</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Custom ID-JAG on PingFederate, Part 3: Managing the Integration with Terraform</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:54:22 +0000</pubDate>
      <link>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-3-managing-the-integration-with-terraform-i8p</link>
      <guid>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-3-managing-the-integration-with-terraform-i8p</guid>
      <description>&lt;p&gt;Once the custom ID-JAG generator worked, the next requirement was to make its PingFederate configuration repeatable.&lt;/p&gt;

&lt;p&gt;The goal was not just to automate a sequence of API calls. We wanted Terraform to own identifiable resources, refresh their state and produce a reviewable plan.&lt;/p&gt;

&lt;p&gt;The result manages eight resources on the local server reporting PingFederate 12.3.3.1. After apply, a follow-up plan returned no changes.&lt;/p&gt;

&lt;p&gt;Code for this series: &lt;a href="https://github.com/darkedges/pf12.3-id-jag-poc" rel="noopener noreferrer"&gt;project repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate plugin deployment from configuration
&lt;/h2&gt;

&lt;p&gt;Terraform configures the installed plugin. It does not deploy the JAR in this project.&lt;/p&gt;

&lt;p&gt;The Java build produces &lt;code&gt;xaa-id-jag-generator.jar&lt;/code&gt;. That file must be placed in the server's &lt;code&gt;server/default/deploy&lt;/code&gt; directory, followed by a restart and verification that the Admin API discovers its descriptor.&lt;/p&gt;

&lt;p&gt;The live test used a copy into the named local container. The repository also includes an optional Compose overlay for a read-only JAR mount. That overlay was syntax-checked, but it was not the deployment mechanism used for the successful live run.&lt;/p&gt;

&lt;p&gt;Keeping these steps separate makes the dependency visible: an API resource cannot configure a plugin that the server has not loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the provider schema, not only its examples
&lt;/h2&gt;

&lt;p&gt;We pinned these provider versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.10, &amp;lt; 2.0"&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pingfederate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pingidentity/pingfederate"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.9.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;restapi&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Mastercard/restapi"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3.0.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspection of the installed Ping provider schema confirmed that version 1.9.0 does not implement token-generator instances or token-exchange generator groups, despite references to those resources in some examples.&lt;/p&gt;

&lt;p&gt;The provider does implement the JWT processor, exchange policy, OAuth clients and generator mapping we needed. We used it for those resources.&lt;/p&gt;

&lt;p&gt;For the missing resources, we used the REST provider against the actual Admin API. Its &lt;a href="https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object" rel="noopener noreferrer"&gt;object resource&lt;/a&gt; supports resource lifecycle operations and refresh, rather than just running a command during apply.&lt;/p&gt;

&lt;p&gt;This split is specific to the pinned versions. Recheck the schema before assuming the same gap exists in a future release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight resources
&lt;/h2&gt;

&lt;p&gt;The complete configuration manages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A JWT Token Processor 2.0 instance for the trusted subject issuer.&lt;/li&gt;
&lt;li&gt;A Token Exchange Processor Policy with subject and &lt;code&gt;azp&lt;/code&gt; issuance criteria.&lt;/li&gt;
&lt;li&gt;A confidential requesting client restricted to token exchange and the approved scope.&lt;/li&gt;
&lt;li&gt;A disabled compatibility client whose ID is the downstream issuer URL.&lt;/li&gt;
&lt;li&gt;The custom ID-JAG generator instance.&lt;/li&gt;
&lt;li&gt;A resource-selected generator group.&lt;/li&gt;
&lt;li&gt;The policy-to-generator attribute mapping.&lt;/li&gt;
&lt;li&gt;One common scope entry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The REST provider owns the generator, generator group and individual scope entry. Everything else uses the Ping provider.&lt;/p&gt;

&lt;p&gt;Managing the scope individually avoids taking ownership of the entire authorization-server settings object. We did not change global expression settings, global key configuration or the global default generator group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the real API resources
&lt;/h2&gt;

&lt;p&gt;The generator-group excerpt looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"restapi_object"&lt;/span&gt; &lt;span class="s2"&gt;"generator_group"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/oauth/tokenExchange/generator/groups"&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_server_additions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xaaGroup"&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XAA explicit resource routing"&lt;/span&gt;
    &lt;span class="nx"&gt;resourceUris&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_resource&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;generatorMappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;requestedTokenType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"urn:ietf:params:oauth:token-type:id-jag"&lt;/span&gt;
      &lt;span class="nx"&gt;tokenGenerator&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;restapi_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;defaultMapping&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The group depends on the generator's resource ID. Its mapping is the default within this group, which the live API required. The group itself is not a global default.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ignore_server_additions&lt;/code&gt; accommodates server-added fields. It is not &lt;code&gt;ignore_all_server_changes&lt;/code&gt;; the configuration should still own the fields it explicitly sets. The observed post-apply result was a clean plan, not a comprehensive test of every possible drift scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the administrator password out of state
&lt;/h2&gt;

&lt;p&gt;The administrator password is a sensitive, ephemeral input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"admin_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ephemeral&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider uses it for authentication. A check of the resulting state confirmed that the supplied administrator password was absent.&lt;/p&gt;

&lt;p&gt;OAuth client secrets are different. They are resource inputs and are stored in state. Marking them sensitive hides their normal display; it does not encrypt the state file.&lt;/p&gt;

&lt;p&gt;The local fixture files and state are ignored by Git. Shared use still needs an encrypted, access-controlled backend, appropriate secret injection and protected local files. A Git ignore rule is not a security boundary.&lt;/p&gt;

&lt;p&gt;TLS verification defaults to enabled. The disposable localhost fixture explicitly opts into a localhost-only certificate exception. Remote configurations must use a trusted certificate or a configured CA file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan before applying
&lt;/h2&gt;

&lt;p&gt;From the repository root, with provider credentials supplied through the documented environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-chdir=terraform'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-chdir=terraform'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-chdir=terraform'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-var-file=../.local/local.tfvars.json'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-out=../.local/xaa.tfplan'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the plan before applying the saved file. On our fresh test server, the initial plan contained eight additions and no existing-resource updates or deletions.&lt;/p&gt;

&lt;p&gt;The first apply exposed server validation requirements described in Part 2. Terraform retained the successfully created resources in state, and the corrected plan completed the remaining configuration. We did not discard state and start creating duplicate objects.&lt;/p&gt;

&lt;p&gt;After the successful apply, &lt;code&gt;plan -detailed-exitcode&lt;/code&gt; returned zero with no changes. Four mocked Terraform tests also passed, checking the restricted configuration and rejecting remote TLS bypass, private JWKS input and shared client secrets.&lt;/p&gt;

&lt;p&gt;Those tests do not prove token issuance. Part 4 covers the separate live runner, the evidence it produced and the work that remains before this could become a production integration.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>oauth</category>
      <category>security</category>
    </item>
    <item>
      <title>Custom ID-JAG on PingFederate, Part 2: Building the Generator and Handling Runtime Constraints</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:53:48 +0000</pubDate>
      <link>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-2-building-the-generator-and-handling-runtime-constraints-20i8</link>
      <guid>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-2-building-the-generator-and-handling-runtime-constraints-20i8</guid>
      <description>&lt;p&gt;The signing code was only one part of getting a custom ID-JAG generator working on PingFederate 12.3.3.1.&lt;/p&gt;

&lt;p&gt;The more revealing work was discovering what the token-exchange pipeline validates before calling the generator, and what it actually passes into the plugin.&lt;/p&gt;

&lt;p&gt;This article describes the implementation that passed the live local proof. It issues an assertion for one fixed destination. It does not configure downstream redemption.&lt;/p&gt;

&lt;p&gt;Code for this series: &lt;a href="https://github.com/darkedges/pf12.3-id-jag-poc" rel="noopener noreferrer"&gt;project repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a small, explicit contract
&lt;/h2&gt;

&lt;p&gt;The final generator receives six mapped attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: the validated subject carried through the exchange policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subject_client_id&lt;/code&gt;: the validated subject token's audience.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subject_exp&lt;/code&gt;: the validated subject token's expiry in Unix seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;approved_scope&lt;/code&gt;: scopes authorized by the trusted policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authenticated_client_id&lt;/code&gt;: PingFederate's authenticated client context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http_request&lt;/code&gt;: PingFederate's actual servlet request context object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is an important separation here. The HTTP request supplies the requested destination and scope, but it does not establish who the client is. Client identity comes from the server's authenticated context.&lt;/p&gt;

&lt;p&gt;We also found that this 12.3.3 token-generator exchange path creates a &lt;code&gt;TokenContext&lt;/code&gt; without populating its input parameters. Relying on &lt;code&gt;getInParameters()&lt;/code&gt; for authentication or routing information would have been incorrect. The implementation uses explicitly mapped subject attributes instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read request parameters without enabling expressions
&lt;/h2&gt;

&lt;p&gt;The first configuration attempted expression-based mappings for &lt;code&gt;audience&lt;/code&gt;, &lt;code&gt;resource&lt;/code&gt; and &lt;code&gt;scope&lt;/code&gt;. The running server rejected those mappings.&lt;/p&gt;

&lt;p&gt;Instead of enabling expressions globally, the final mapping passes Context &lt;code&gt;HttpRequest&lt;/code&gt; directly to the plugin. The generator extracts the underlying &lt;code&gt;HttpServletRequest&lt;/code&gt; object and validates its parameters in Java.&lt;/p&gt;

&lt;p&gt;The essential helper is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;parameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getParameterValues&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Missing or repeated request parameter: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;nonempty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nonempty()&lt;/code&gt; also rejects blank values and control characters. This is an excerpt from the plugin, not a standalone implementation.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;getParameterValues()&lt;/code&gt; matters. Selecting only the first value could hide duplicate parameters. The live suite confirmed rejection of repeated audience, resource and scope parameters, including repeated identical values.&lt;/p&gt;

&lt;p&gt;This is a deliberately strict request profile. Do not assume that accepting multiple resources or audiences can be added without revisiting the policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the caller to the subject token
&lt;/h2&gt;

&lt;p&gt;The generator compares both &lt;code&gt;authenticated_client_id&lt;/code&gt; and &lt;code&gt;subject_client_id&lt;/code&gt; with the configured requesting client.&lt;/p&gt;

&lt;p&gt;The JWT processor validates the subject token's audience. The policy additionally requires &lt;code&gt;azp&lt;/code&gt; to match the requesting client and &lt;code&gt;sub&lt;/code&gt; to match the authorized test subject.&lt;/p&gt;

&lt;p&gt;Our proof requires &lt;code&gt;azp&lt;/code&gt; even when an intended real issuer might omit it. That is a restriction of this implementation, not a claim that every ID token must contain the claim. Real issuer integration needs an explicit review of those semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat approved scope as authorization data
&lt;/h2&gt;

&lt;p&gt;The requested scopes must be contained in both the generator's configured scope ceiling and the policy-provided &lt;code&gt;approved_scope&lt;/code&gt; set.&lt;/p&gt;

&lt;p&gt;Copying the incoming scope directly into &lt;code&gt;approved_scope&lt;/code&gt; would defeat that check.&lt;/p&gt;

&lt;p&gt;In this lab, the approved scope is a fixed value behind issuance criteria for one authorized subject and client. Production deployment needs a real entitlement decision for the user, client and destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Satisfy the audience check before the generator
&lt;/h2&gt;

&lt;p&gt;The live server validates each token-exchange &lt;code&gt;audience&lt;/code&gt; as a local OAuth client ID. An unknown value fails before the custom generator can run.&lt;/p&gt;

&lt;p&gt;Our outgoing assertion needs the target authorization-server issuer as its audience. To preserve that value, we created a local compatibility registration whose client ID is exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://target.example.test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not the requesting client and not the client registration at the downstream server.&lt;/p&gt;

&lt;p&gt;The compatibility client remains disabled. It has an independent unused secret and only the &lt;code&gt;ACCESS_TOKEN_VALIDATION&lt;/code&gt; grant, because the Terraform provider requires a nonempty grant list.&lt;/p&gt;

&lt;p&gt;Two live checks established the boundary: issuance succeeded with that registration disabled, and an authentication attempt using it was rejected. This is observed behavior of the tested server, not a blanket compatibility promise for every PF version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route by resource and sign with managed keys
&lt;/h2&gt;

&lt;p&gt;The generator group maps the exact resource URI to the ID-JAG requested token type. PingFederate required one default mapping inside the group. We configured that mapping without making the group a global default.&lt;/p&gt;

&lt;p&gt;The plugin gets the current RS256 key through the public SDK accessor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JwksEndpointKeyAccessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getCurrentRsaKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RS256"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It signs with &lt;code&gt;typ&lt;/code&gt; set to &lt;code&gt;oauth-id-jag+jwt&lt;/code&gt;, includes the key's &lt;code&gt;kid&lt;/code&gt;, and creates a fresh &lt;code&gt;jti&lt;/code&gt;. Expiry is the earlier of the configured lifetime and the validated subject-token expiry.&lt;/p&gt;

&lt;p&gt;The compact JWT is returned as a &lt;code&gt;StringSecurityToken&lt;/code&gt;. The live server preserved that string without an additional Base64 wrapper, and the signature verified against its published JWKS.&lt;/p&gt;

&lt;p&gt;Generating a unique &lt;code&gt;jti&lt;/code&gt; is not replay prevention. The receiver still needs to enforce replay handling when the assertion is redeemed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the remaining boundary visible
&lt;/h2&gt;

&lt;p&gt;This implementation validated the issuing side with a locally trusted subject fixture. Real issuer login, downstream acceptance, production entitlements and multi-node behavior remain separate work.&lt;/p&gt;

&lt;p&gt;Part 3 makes the working configuration repeatable with Terraform, including the resources that the pinned Ping provider does not expose directly.&lt;/p&gt;

</description>
      <category>java</category>
      <category>oauth</category>
      <category>security</category>
      <category>identity</category>
    </item>
    <item>
      <title>Custom ID-JAG on PingFederate, Part 1: Can PingFederate 12.3.3 Issue an ID-JAG?</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:52:59 +0000</pubDate>
      <link>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-1-can-pingfederate-1233-issue-an-id-jag-1eom</link>
      <guid>https://dev.to/darkedges/custom-id-jag-on-pingfederate-part-1-can-pingfederate-1233-issue-an-id-jag-1eom</guid>
      <description>&lt;p&gt;PF ID-JAG: Can PingFederate 12.3.3 issue an ID-JAG through a custom token generator?&lt;/p&gt;

&lt;p&gt;For the fixed-destination proof described here, the answer is yes. A live PingFederate server returned the assertion, and its signature verified against that server's published JWKS.&lt;/p&gt;

&lt;p&gt;That result matters, but so does its boundary. It is not evidence that an arbitrary application can accept the assertion, that a complete Cross-App Access deployment exists, or that the implementation conforms to every requirement of the evolving specification.&lt;/p&gt;

&lt;p&gt;This series follows the path from inspecting the extension points to applying Terraform and testing the real token endpoint.&lt;/p&gt;

&lt;p&gt;Code for this series: &lt;a href="https://github.com/darkedges/pf12.3-id-jag-poc" rel="noopener noreferrer"&gt;project repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we were trying to build
&lt;/h2&gt;

&lt;p&gt;Cross-App Access, or XAA, is the pattern described by the Identity Assertion JWT Authorization Grant draft. An application obtains an ID-JAG through token exchange, then presents that assertion to a downstream authorization server to request an access token. The draft connects OAuth token exchange with the JWT authorization grant flow. This project uses &lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant-04" rel="noopener noreferrer"&gt;draft-04&lt;/a&gt; as its reference, not a finalized standard.&lt;/p&gt;

&lt;p&gt;Our implementation focuses on the first exchange. The intended sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The requesting client authenticates to PingFederate and supplies a subject token.&lt;/li&gt;
&lt;li&gt;PingFederate validates the token and applies an authorization policy.&lt;/li&gt;
&lt;li&gt;A custom generator issues an ID-JAG for a fixed downstream destination.&lt;/li&gt;
&lt;li&gt;The application would redeem that assertion at the downstream authorization server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the first three steps were exercised against the live server. Step four remains integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Missing native support was not the same as a missing extension path
&lt;/h2&gt;

&lt;p&gt;An early assumption in this project was that a generic JWT access-token flow would be the practical substitute. That assumption did not survive inspection of the actual 12.3.3 classes.&lt;/p&gt;

&lt;p&gt;The relevant question was more precise: could the token-exchange pipeline route a custom requested token type to a token generator and preserve its output?&lt;/p&gt;

&lt;p&gt;The Java extension point exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;SecurityToken&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TokenContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server also has token-exchange generator groups and mappings. Ping's &lt;a href="https://docs.pingidentity.com/pingfederate/12.3/administrators_reference_guide/pf_creating_token_exchange_generator_groups.html" rel="noopener noreferrer"&gt;generator-group documentation&lt;/a&gt; describes associating requested token types with generator instances and routing requests using resource URIs.&lt;/p&gt;

&lt;p&gt;We inspected and exercised the request parser, resource selector, generator mapping and response serializer from the actual local installation. That established an offline path worth testing on a running server.&lt;/p&gt;

&lt;p&gt;It did not establish a working deployment yet. Client authentication, plugin discovery, attribute mappings and key retrieval still had to work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token processor and generator have different jobs
&lt;/h2&gt;

&lt;p&gt;The implementation uses the built-in JWT Token Processor 2.0 for inbound validation and a custom Java generator for outbound issuance.&lt;/p&gt;

&lt;p&gt;The processor validates the configured issuer, signature, audience and time claims. A Token Exchange Processor Policy then restricts the allowed subject and checks the authorized-party claim, &lt;code&gt;azp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The generator checks the authenticated client, destination, scope and remaining subject-token lifetime before signing the outgoing assertion.&lt;/p&gt;

&lt;p&gt;This division is important. Producing a signed JWT does not establish that its subject is authorized to access another application's API. The authorization decision needs a trusted source and an explicit policy.&lt;/p&gt;

&lt;p&gt;For the lab, that policy authorizes exactly one test subject. It is intentionally not a substitute for production entitlements.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deliberately narrow proof
&lt;/h2&gt;

&lt;p&gt;The tested setup has one requesting client, one target authorization-server issuer, one resource, one allowed scope and RS256 signing. Assertion lifetime is capped at 300 seconds and cannot exceed the validated subject token's expiry.&lt;/p&gt;

&lt;p&gt;The build uses the PF 12.3.3 installation and SDK. The live Admin API reports version 12.3.3.1.&lt;/p&gt;

&lt;p&gt;The inbound token is a locally signed fixture whose public key is explicitly trusted by the JWT processor. That gives us controlled positive and negative tests without pretending to have completed a real OIDC login.&lt;/p&gt;

&lt;p&gt;The target issuer and resource use reserved example domains. No downstream authorization server or target client was provisioned by this proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the live result established
&lt;/h2&gt;

&lt;p&gt;The successful response contained the unchanged compact JWT in &lt;code&gt;access_token&lt;/code&gt;, the ID-JAG URI in &lt;code&gt;issued_token_type&lt;/code&gt;, &lt;code&gt;token_type&lt;/code&gt; set to &lt;code&gt;N_A&lt;/code&gt;, and a bounded &lt;code&gt;expires_in&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;The runner verified the signature using PingFederate's live JWKS endpoint. It also checked the intended subject, issuer, destination, client, resource and scope.&lt;/p&gt;

&lt;p&gt;Across the live run, 27 checks passed. Those included rejection cases, not just successful issuance. The detailed test coverage is the subject of Part 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;There is a useful distinction between a product feature being available out of the box and a product having extension points capable of implementing a constrained version of it.&lt;/p&gt;

&lt;p&gt;For this server, the custom-generator path was viable. Getting it to work required understanding the validation that happens before the generator, the attributes that actually reach it and the format the server returns afterward.&lt;/p&gt;

&lt;p&gt;Part 2 covers those runtime details, including an audience lookup that initially looked unrelated to the outgoing assertion and a way to avoid expression-based request mappings.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>security</category>
      <category>java</category>
      <category>identity</category>
    </item>
    <item>
      <title>Building One Tap for PingFederate, Part 3: An Automated Narrated Demo Video</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:55:36 +0000</pubDate>
      <link>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-3-an-automated-narrated-demo-video-4176</link>
      <guid>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-3-an-automated-narrated-demo-video-4176</guid>
      <description>&lt;p&gt;Authentication demonstrations are difficult to record reliably. A live flow&lt;br&gt;
depends on cookies, existing sessions, certificate trust, test passwords, and&lt;br&gt;
network timing. It can also expose credentials or tokens in a recording.&lt;/p&gt;

&lt;p&gt;For this project, I built a deterministic presentation that explains the real&lt;br&gt;
behavior without depending on live secrets. The implementation is available at&lt;br&gt;
&lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;github.com/darkedges/pingfedonetap&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The storyboard
&lt;/h2&gt;

&lt;p&gt;The 16:9 tour contains six timed scenes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A fresh visitor sees no unsolicited account popup.&lt;/li&gt;
&lt;li&gt;The visitor starts an interactive PingFederate login.&lt;/li&gt;
&lt;li&gt;The OIDC callback establishes the application session.&lt;/li&gt;
&lt;li&gt;Application-only logout clears local state.&lt;/li&gt;
&lt;li&gt;The chooser displays two remembered accounts.&lt;/li&gt;
&lt;li&gt;Selecting the second account reuses the PingFederate session without a
password prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The presentation uses mock credentials and token summaries. It demonstrates&lt;br&gt;
state transitions but never handles a real password, authorization code, or&lt;br&gt;
token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make demo-tour
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page supports autoplay, looping, playback speed, hidden controls, Space to&lt;br&gt;
pause, and R to restart. This makes it useful both for rehearsal and automated&lt;br&gt;
capture.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generate a timed AI voiceover
&lt;/h2&gt;

&lt;p&gt;The narration uses the Apache-2.0 licensed &lt;code&gt;hexgrad/Kokoro-82M&lt;/code&gt; model from&lt;br&gt;
Hugging Face. The selected &lt;code&gt;af_heart&lt;/code&gt; voice is clear for short technical&lt;br&gt;
narration.&lt;/p&gt;

&lt;p&gt;The source script is a JSON file with a start time, scene label, and sentence&lt;br&gt;
for each cue. The generator synthesizes every sentence, measures its actual&lt;br&gt;
duration, and fails if it would overlap the next cue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make voiceover
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;artifacts/one-tap-demo-voiceover.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Solving PyTorch memory growth
&lt;/h3&gt;

&lt;p&gt;An early implementation kept one Kokoro pipeline alive while generating all&lt;br&gt;
six sentences. On a constrained Windows development machine, CPU memory grew&lt;br&gt;
until PyTorch failed during a later cue.&lt;/p&gt;

&lt;p&gt;The final generator starts one worker process per cue. Each worker loads the&lt;br&gt;
model, writes a temporary floating-point WAV, and exits. Process termination&lt;br&gt;
returns PyTorch's working memory to the operating system before the next cue&lt;br&gt;
starts. The parent process then validates timing and mixes the small audio&lt;br&gt;
segments into a 34-second, 24 kHz PCM WAV.&lt;/p&gt;

&lt;p&gt;This approach is slower, but its memory behavior is predictable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Record the browser automatically
&lt;/h2&gt;

&lt;p&gt;Playwright opens a headless Chromium context with both viewport and recording&lt;br&gt;
size fixed at 1280 x 720. Autoplay is disabled so the recorder controls the&lt;br&gt;
exact start.&lt;/p&gt;

&lt;p&gt;The capture process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Opens the presentation and verifies the stage dimensions.&lt;/li&gt;
&lt;li&gt;Waits for the page to settle.&lt;/li&gt;
&lt;li&gt;Presses R to start the tour.&lt;/li&gt;
&lt;li&gt;Waits until the tour marks the final scene complete.&lt;/li&gt;
&lt;li&gt;Holds the final signed-in frame until the 34-second narration ends.&lt;/li&gt;
&lt;li&gt;Closes the browser context so Playwright finalizes the WebM.&lt;/li&gt;
&lt;li&gt;Trims browser setup frames.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Mux a delivery-ready MP4
&lt;/h2&gt;

&lt;p&gt;The project uses &lt;code&gt;imageio-ffmpeg&lt;/code&gt;, whose platform wheel includes an FFmpeg&lt;br&gt;
executable. FFmpeg converts the silent VP8 WebM to H.264 and adds the WAV as AAC&lt;br&gt;
audio.&lt;/p&gt;

&lt;p&gt;The complete pipeline is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make demo-video
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;artifacts/one-tap-demo-screen.webm
artifacts/one-tap-demo-final.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final file is 1280 x 720, 30 frames per second, 34 seconds long, with H.264&lt;br&gt;
video, AAC audio, and the MP4 metadata moved to the front for progressive web&lt;br&gt;
playback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why keep the deterministic tour separate
&lt;/h2&gt;

&lt;p&gt;The narrated presentation is not a replacement for integration tests. The live&lt;br&gt;
demo still verifies the adapter endpoint, cookie decoding, OIDC callback, PKCE,&lt;br&gt;
and session behavior. The deterministic tour has a different purpose: it makes&lt;br&gt;
the explanation repeatable, safe to publish, and easy to regenerate after a UI&lt;br&gt;
change.&lt;/p&gt;

&lt;p&gt;That separation produced a better test tool and a better communication tool.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;https://github.com/darkedges/pingfedonetap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the published demo: &lt;a href="https://youtu.be/KuVNjWiZrAk" rel="noopener noreferrer"&gt;https://youtu.be/KuVNjWiZrAk&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Playwright #Python #AI #FFmpeg #PingFederate #DevOps
&lt;/h1&gt;

</description>
      <category>playwright</category>
      <category>python</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building One Tap for PingFederate, Part 2: Docker, Terraform, and OIDC with PKCE</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:54:04 +0000</pubDate>
      <link>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-2-docker-terraform-and-oidc-with-pkce-3neg</link>
      <guid>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-2-docker-terraform-and-oidc-with-pkce-3neg</guid>
      <description>&lt;p&gt;The account chooser needs more than a widget and a Java adapter. PingFederate&lt;br&gt;
must have a complete authentication policy, a credential validator, a reusable&lt;br&gt;
authentication session, and an OIDC client. This article makes that environment&lt;br&gt;
repeatable with Docker, Terraform, and Make.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;github.com/darkedges/pingfedonetap&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Bake the adapter into the PingFederate image
&lt;/h2&gt;

&lt;p&gt;The Maven build produces the adapter JAR. A PowerShell staging script creates a&lt;br&gt;
server profile overlay with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker/pingfederate-profile/
  instance/server/default/
    deploy/one-tap-status-adapter.jar
    conf/template/one-tap-status-template.html
    conf/language-packs/one-tap-status-template.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dockerfile copies that overlay to &lt;code&gt;/opt/in/&lt;/code&gt; in the official&lt;br&gt;
PingFederate image. At startup, the Ping container downloads Ping Identity's&lt;br&gt;
getting-started profile and merges the local overlay into the runtime under&lt;br&gt;
&lt;code&gt;/opt/out/instance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important point is that the JAR is part of the image input. It is not&lt;br&gt;
copied manually into a running container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make docker-image
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keep license credentials out of source
&lt;/h2&gt;

&lt;p&gt;The container obtains its development license at startup. Put the Ping DevOps&lt;br&gt;
credentials in environment variables or an ignored &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PING_IDENTITY_DEVOPS_USER=replace-me
PING_IDENTITY_DEVOPS_KEY=replace-me
ONE_TAP_ALLOWED_ORIGINS=http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not commit these values. The credentials are needed when the container&lt;br&gt;
starts, not when the image is built.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make docker-up
make docker-logs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Provision the authentication chain
&lt;/h2&gt;

&lt;p&gt;The Terraform configuration creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Simple Username Password Credential Validator for disposable test users.&lt;/li&gt;
&lt;li&gt;An HTML Form IdP Adapter backed by that validator.&lt;/li&gt;
&lt;li&gt;An Identifier First Adapter that remembers up to five identifiers for 30
days.&lt;/li&gt;
&lt;li&gt;The custom One Tap status adapter with an exact origin allowlist.&lt;/li&gt;
&lt;li&gt;An enabled IdP authentication policy.&lt;/li&gt;
&lt;li&gt;An authentication-session policy for the HTML Form adapter.&lt;/li&gt;
&lt;li&gt;OAuth and OIDC mappings.&lt;/li&gt;
&lt;li&gt;A public OIDC client restricted to Authorization Code with PKCE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interactive policy is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identifier First
  Success: pass subject as incoming username
    HTML Form
      Success: complete authentication
      Failure: fail
  Failure: fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status adapter is not part of this tree. Adding a status-only adapter to the&lt;br&gt;
interactive policy would leave authentication in progress and prevent the flow&lt;br&gt;
from completing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the authentication-session policy matters
&lt;/h2&gt;

&lt;p&gt;Remembering an identifier does not remove the password requirement. The HTML&lt;br&gt;
Form adapter must also have a reusable PingFederate authentication session.&lt;/p&gt;

&lt;p&gt;The local policy uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Persistent session: true
Idle timeout: 60 minutes
Maximum timeout: 480 minutes
Device type: private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the first successful password authentication, a later OIDC request can&lt;br&gt;
reuse this session. That is what lets the remembered-account selection finish&lt;br&gt;
without prompting for the password again.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configure a browser public client correctly
&lt;/h2&gt;

&lt;p&gt;The demo client uses these settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client ID: one-tap-demo
Grant: Authorization Code
Client authentication: none
PKCE: required
Scope: openid
Redirect URI: http://localhost:8080/callback.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redirect URIs are exact matches. A different port, path, or trailing slash must&lt;br&gt;
be registered explicitly.&lt;/p&gt;

&lt;p&gt;The callback validates the OIDC state and exchanges the code with the PKCE&lt;br&gt;
verifier. The demo shows only a sanitized result and does not retain the code,&lt;br&gt;
verifier, access token, or ID token.&lt;/p&gt;
&lt;h2&gt;
  
  
  Apply the configuration
&lt;/h2&gt;

&lt;p&gt;Ping DevOps license credentials and Terraform administrative credentials are&lt;br&gt;
different concerns. Set the provider credentials in the current shell, then&lt;br&gt;
use an ignored &lt;code&gt;terraform.tfvars&lt;/code&gt; for test users and local options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;make terraform-init
make terraform-plan
make terraform-apply
make demo-up
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; after trusting the local development certificate&lt;br&gt;
for &lt;code&gt;https://localhost:9031&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production boundaries
&lt;/h2&gt;

&lt;p&gt;This environment is a local demonstration. For a real deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store Terraform state in an encrypted, access-controlled backend.&lt;/li&gt;
&lt;li&gt;Use an enterprise credential validator rather than local test users.&lt;/li&gt;
&lt;li&gt;Remove development TLS trust overrides.&lt;/li&gt;
&lt;li&gt;Validate ID token signatures and claims in a backend.&lt;/li&gt;
&lt;li&gt;Use production origins and exact redirect URIs.&lt;/li&gt;
&lt;li&gt;Define explicit identity provider logout behavior separately from
application logout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In part 3, we will turn the working flow into a deterministic narrated video&lt;br&gt;
with Kokoro, Playwright, and FFmpeg.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;https://github.com/darkedges/pingfedonetap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video demo: &lt;a href="https://youtu.be/KuVNjWiZrAk" rel="noopener noreferrer"&gt;https://youtu.be/KuVNjWiZrAk&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Terraform #Docker #OIDC #PingFederate #DevOps
&lt;/h1&gt;

</description>
      <category>terraform</category>
      <category>docker</category>
      <category>oidc</category>
      <category>pingidentity</category>
    </item>
    <item>
      <title>Building One Tap for PingFederate, Part 1: Architecture and the Secure Account Bridge</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:53:15 +0000</pubDate>
      <link>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-1-architecture-and-the-secure-account-bridge-3ed4</link>
      <guid>https://dev.to/darkedges/building-one-tap-for-pingfederate-part-1-architecture-and-the-secure-account-bridge-3ed4</guid>
      <description>&lt;p&gt;Modern sign-in experiences often remember accounts and let a returning user&lt;br&gt;
continue with one click. Reproducing that pattern with PingFederate requires&lt;br&gt;
more than drawing an account picker. The browser, PingFederate, and the web&lt;br&gt;
application each own different state, and treating those states as one session&lt;br&gt;
causes repeated password prompts, stale panels, and broken logout behavior.&lt;/p&gt;

&lt;p&gt;This series explains a working local implementation for PingFederate 12.3.3.&lt;br&gt;
The complete source is available at&lt;br&gt;
&lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;github.com/darkedges/pingfedonetap&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The behavior we want
&lt;/h2&gt;

&lt;p&gt;The target experience has four rules:&lt;/p&gt;

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

&lt;p&gt;The important distinction is that a remembered account is not automatically a&lt;br&gt;
passwordless account. The account chooser answers "who might sign in?" A valid&lt;br&gt;
PingFederate authentication session answers "has this user already proved who&lt;br&gt;
they are?" The application session answers "is this browser tab currently&lt;br&gt;
signed in to this application?"&lt;/p&gt;
&lt;h2&gt;
  
  
  The three state owners
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Web application
  Owns the local signed-in state and OIDC callback result

PingFederate authentication session
  Determines whether another password challenge is necessary

Identifier First adapter cookie
  Remembers identifiers that completed authentication successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These states must be coordinated, not merged.&lt;/p&gt;

&lt;p&gt;When the application receives tokens, it suppresses the chooser and hides its&lt;br&gt;
sign-in button. When the user selects "Log out of demo," the application clears&lt;br&gt;
its own state and resumes the chooser. It deliberately does not terminate the&lt;br&gt;
PingFederate session. That is why selecting a remembered account can complete&lt;br&gt;
silently after application logout.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the browser cannot simply read the account cookie
&lt;/h2&gt;

&lt;p&gt;PingFederate's Identifier First adapter owns a persistent HttpOnly cookie named&lt;br&gt;
&lt;code&gt;identifierFirstAdapter.previous.subjects&lt;/code&gt;. HttpOnly is correct because page&lt;br&gt;
JavaScript should not read authentication cookies.&lt;/p&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Demo application
  |
  | hidden sandboxed iframe with nonce
  v
PingFederate /ext/one-tap/status
  |
  | reads HttpOnly Identifier First cookie
  v
Allowed parent origin receives sanitized users via postMessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authentication flow
&lt;/h2&gt;

&lt;p&gt;The first login uses an OIDC Authorization Code flow with PKCE:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application sends the browser to PingFederate.&lt;/li&gt;
&lt;li&gt;Identifier First collects or selects the identifier.&lt;/li&gt;
&lt;li&gt;The HTML Form adapter validates the password.&lt;/li&gt;
&lt;li&gt;PingFederate records the successful identifier and establishes its session.&lt;/li&gt;
&lt;li&gt;The callback validates state and exchanges the code with PKCE.&lt;/li&gt;
&lt;li&gt;The application marks itself signed in and suppresses the account chooser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a remembered account, the widget first attempts authorization with&lt;br&gt;
&lt;code&gt;prompt=none&lt;/code&gt;. If PingFederate can reuse its session, tokens return without a&lt;br&gt;
password prompt. If silent authentication cannot complete, the widget falls&lt;br&gt;
back to an interactive top-level request without forcing &lt;code&gt;prompt=login&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build the status bridge
&lt;/h2&gt;

&lt;p&gt;The custom &lt;code&gt;OneTapStatusAdapter&lt;/code&gt; registers this extension endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ext/one-tap/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The endpoint is status-only. It is not inserted into the interactive&lt;br&gt;
authentication policy and remains separate from the Identifier First and HTML&lt;br&gt;
Form sequence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Decode the native cookie
&lt;/h2&gt;

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

&lt;p&gt;Supporting PingFederate's native representation was important. Inventing a&lt;br&gt;
second account cookie would have created another source of identity state and&lt;br&gt;
made lifecycle behavior harder to reason about.&lt;/p&gt;
&lt;h2&gt;
  
  
  Secure the cross-origin response
&lt;/h2&gt;

&lt;p&gt;A cross-origin account bridge needs several controls working together.&lt;/p&gt;
&lt;h3&gt;
  
  
  Exact origin allowlist
&lt;/h3&gt;

&lt;p&gt;Configured origins are normalized to a scheme, host, and optional non-default&lt;br&gt;
port. User information, paths, query strings, and fragments are rejected.&lt;/p&gt;

&lt;p&gt;The same allowlist controls CORS, CSP, and the permitted &lt;code&gt;postMessage&lt;/code&gt; target.&lt;/p&gt;
&lt;h3&gt;
  
  
  Per-request nonce
&lt;/h3&gt;

&lt;p&gt;The widget generates a random nonce and includes it as &lt;code&gt;pfOtNonce&lt;/code&gt;. The adapter&lt;br&gt;
accepts only 32 to 128 hexadecimal characters and echoes the value in the&lt;br&gt;
bridge message.&lt;/p&gt;

&lt;p&gt;The parent accepts the response only when the origin, source window, message&lt;br&gt;
type, and nonce all match the iframe request it created.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sandboxed iframe
&lt;/h3&gt;

&lt;p&gt;The widget creates an invisible iframe with &lt;code&gt;sandbox="allow-scripts"&lt;/code&gt;. The&lt;br&gt;
bridge can execute its small script, but it cannot submit forms, navigate the&lt;br&gt;
top-level page, or gain broader browser capabilities.&lt;/p&gt;
&lt;h3&gt;
  
  
  Explicit postMessage target
&lt;/h3&gt;

&lt;p&gt;The bridge never posts to &lt;code&gt;"*"&lt;/code&gt;. It checks &lt;code&gt;document.referrer&lt;/code&gt; against the&lt;br&gt;
allowlist and posts only to an accepted origin. The parent independently&lt;br&gt;
validates the sender origin and nonce.&lt;/p&gt;
&lt;h3&gt;
  
  
  Narrow framing policy
&lt;/h3&gt;

&lt;p&gt;PingFederate normally emits &lt;code&gt;X-Frame-Options: SAMEORIGIN&lt;/code&gt;. That blocks the&lt;br&gt;
intentional cross-origin iframe. The dedicated endpoint removes that header&lt;br&gt;
and replaces it with a narrow policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy: frame-ancestors http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also returns &lt;code&gt;Cache-Control: no-store&lt;/code&gt; so account status is not cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid the first-request 404
&lt;/h2&gt;

&lt;p&gt;An extension handler registered only when an adapter instance is first used&lt;br&gt;
can return 404 when the widget calls it before any authentication flow has&lt;br&gt;
initialized the adapter.&lt;/p&gt;

&lt;p&gt;The implementation registers the handler in the adapter constructor and again&lt;br&gt;
during configuration. The container also provides&lt;br&gt;
&lt;code&gt;ONE_TAP_ALLOWED_ORIGINS&lt;/code&gt; during plugin discovery so early registration has a&lt;br&gt;
safe origin policy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Give the host explicit controls
&lt;/h2&gt;

&lt;p&gt;The browser widget exposes a deliberately small API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;PfOneTap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;PfOneTap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suppress&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;PfOneTap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;PfOneTap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;PfOneTap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host calls &lt;code&gt;suppress()&lt;/code&gt; after establishing its application session. On&lt;br&gt;
application logout, it calls &lt;code&gt;resume()&lt;/code&gt; and &lt;code&gt;refresh()&lt;/code&gt; to request fresh status&lt;br&gt;
with a new nonce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design and security lessons
&lt;/h2&gt;

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

&lt;p&gt;In part 2, we will package the adapter with Docker and provision the complete&lt;br&gt;
authentication and OIDC configuration with Terraform.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/darkedges/pingfedonetap" rel="noopener noreferrer"&gt;https://github.com/darkedges/pingfedonetap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video demo: &lt;a href="https://youtu.be/KuVNjWiZrAk" rel="noopener noreferrer"&gt;https://youtu.be/KuVNjWiZrAk&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  PingIdentity #PingFederate #OIDC #Authentication #Identity
&lt;/h1&gt;

</description>
      <category>pingidentity</category>
      <category>oidc</category>
      <category>authentication</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Trusted AI Agent Transactions, Part 5: End-to-End Proof</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:03:55 +0000</pubDate>
      <link>https://dev.to/darkedges/trusted-ai-agent-transactions-part-5-end-to-end-proof-44b5</link>
      <guid>https://dev.to/darkedges/trusted-ai-agent-transactions-part-5-end-to-end-proof-44b5</guid>
      <description>&lt;h2&gt;
  
  
  Building and proving the complete request path
&lt;/h2&gt;

&lt;p&gt;The previous articles covered the identity model, &lt;a href="//02-pingfederate-token-exchange.md"&gt;PingFederate token exchange&lt;/a&gt;, &lt;a href="//03-spire-workload-identity.md"&gt;SPIRE workload identity&lt;/a&gt;, and &lt;a href="//04-pingauthorize-policy.md"&gt;PingAuthorize policy&lt;/a&gt;. This final part connects them into one testable application path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application flow
&lt;/h2&gt;

&lt;p&gt;The browser workbench signs the user in through PingFederate with OAuth 2.0 Authorization Code and PKCE. The application never collects the user's PingFederate password.&lt;/p&gt;

&lt;p&gt;After sign-in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the demo agent receives the user access token&lt;/li&gt;
&lt;li&gt;the agent obtains a JWT-SVID from SPIRE&lt;/li&gt;
&lt;li&gt;PingFederate, acting as the TTS, exchanges the subject and actor evidence for a short-lived Txn-Token&lt;/li&gt;
&lt;li&gt;the agent sends that token in the &lt;code&gt;Txn-Token&lt;/code&gt; header while calling the MCP gateway over SPIFFE mTLS&lt;/li&gt;
&lt;li&gt;the gateway validates the transaction token and immediate caller&lt;/li&gt;
&lt;li&gt;OPA or PingAuthorize evaluates the verified target and tool request&lt;/li&gt;
&lt;li&gt;the gateway forwards the unchanged transaction token to the MCP server over mTLS&lt;/li&gt;
&lt;li&gt;the MCP server calls the protected API over another authenticated mTLS hop&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The original user access token does not travel beyond the exchange. The Txn-Token does not become a browser session token, does not use the &lt;code&gt;Authorization&lt;/code&gt; header for internal propagation, and is not rewritten by intermediaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A UI that shows evidence without exposing credentials
&lt;/h2&gt;

&lt;p&gt;The workbench uses two-thirds of the screen for service interaction and one-third for a selectable audit trail. Events correlate the same transaction ID across the agent, gateway, MCP server, API, and authorization decision.&lt;/p&gt;

&lt;p&gt;Selecting an event can show safe request and response metadata plus decoded, allowlisted token claims. It must not expose raw access tokens, JWT-SVIDs, transaction JWTs, authorization headers, cookies, authorization codes, client secrets, refresh tokens, private keys, or sensitive tool arguments.&lt;/p&gt;

&lt;p&gt;Decoded claims are evidence for a human operator, not a new authorization input. Services authorize only after cryptographic validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository-owned product startup
&lt;/h2&gt;

&lt;p&gt;Both Ping products use digest-pinned images and read-only repository-owned profile overlays.&lt;/p&gt;

&lt;p&gt;PingFederate startup builds and tests the custom token processor plugin. Its Terraform configuration remains a separate explicit step.&lt;/p&gt;

&lt;p&gt;PingAuthorize starts with a repository-owned deployment package selected during first setup. The package is mounted read-only, and the container joins only the expected local application bridge network.&lt;/p&gt;

&lt;p&gt;Secrets, licenses, generated certificates, private keys, Terraform state, and discovery output remain outside Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the security statement
&lt;/h2&gt;

&lt;p&gt;The main authorization statement is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verified transaction context
AND verified immediate caller
AND target policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The end-to-end suite therefore needs more than a success case. It proves rejection for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;forged logical agent identity&lt;/li&gt;
&lt;li&gt;wrong SPIFFE workload&lt;/li&gt;
&lt;li&gt;expired transaction token&lt;/li&gt;
&lt;li&gt;wrong audience&lt;/li&gt;
&lt;li&gt;unapproved MCP target&lt;/li&gt;
&lt;li&gt;stolen transaction token presented by the wrong mTLS caller&lt;/li&gt;
&lt;li&gt;direct agent-to-API access when only the MCP server is allowed&lt;/li&gt;
&lt;li&gt;malformed or contradictory authorization decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also proves that one transaction ID appears across all expected hops and that captured logs contain no raw token material.&lt;/p&gt;

&lt;p&gt;The PingFederate clean-bootstrap test goes further by recreating the product on an isolated volume and Terraform state, applying managed TLS, completing a live exchange, rejecting a tampered actor token, and cleaning only its randomly named resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;Agent security is not solved by adding an &lt;code&gt;agent_id&lt;/code&gt; claim or passing a user token farther downstream. The implementation needs distinct evidence for the user, logical agent, workload, transaction, and immediate caller.&lt;/p&gt;

&lt;p&gt;PingFederate provides the controlled delegation boundary. SPIRE proves runtime identity. PingAuthorize evaluates the verified action context. The gateway preserves the boundary between identity verification, routing, and policy enforcement.&lt;/p&gt;

&lt;p&gt;Together, they make an agent action explainable and testable: who authorized it, which agent was approved, which workload ran it, why the transaction exists, which service made each call, and which policy allowed or denied the operation.&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="//04-pingauthorize-policy.md"&gt;Making policy decisions with PingAuthorize&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Return to the &lt;a href="//README.md"&gt;series index&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/darkedges/pf-tts" rel="noopener noreferrer"&gt;https://github.com/darkedges/pf-tts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>pingidentity</category>
      <category>mcp</category>
      <category>security</category>
    </item>
    <item>
      <title>Trusted AI Agent Transactions, Part 4: PingAuthorize Policy Decisions</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:03:22 +0000</pubDate>
      <link>https://dev.to/darkedges/trusted-ai-agent-transactions-part-4-pingauthorize-policy-decisions-3ik8</link>
      <guid>https://dev.to/darkedges/trusted-ai-agent-transactions-part-4-pingauthorize-policy-decisions-3ik8</guid>
      <description>&lt;h2&gt;
  
  
  Making policy decisions with PingAuthorize
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//03-spire-workload-identity.md"&gt;Part 3&lt;/a&gt; established the transaction context and immediate caller identity. Authorization still needs to answer a separate question: may this verified combination invoke this target and tool for this purpose?&lt;/p&gt;

&lt;p&gt;PingAuthorize is integrated as a remote policy decision point at the MCP gateway. It does not verify the transaction JWT or the mTLS connection. Those checks happen before policy evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only verified input crosses the policy boundary
&lt;/h2&gt;

&lt;p&gt;The gateway constructs a typed policy request from values it has already verified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user identity&lt;/li&gt;
&lt;li&gt;logical agent identity&lt;/li&gt;
&lt;li&gt;agent instance identity&lt;/li&gt;
&lt;li&gt;original workload SPIFFE ID&lt;/li&gt;
&lt;li&gt;immediate caller SPIFFE ID&lt;/li&gt;
&lt;li&gt;transaction ID and purpose&lt;/li&gt;
&lt;li&gt;scopes&lt;/li&gt;
&lt;li&gt;MCP target&lt;/li&gt;
&lt;li&gt;tool name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser fields and MCP request fields are not trusted as identity evidence. The target and tool come from the gateway's validated route, not an arbitrary policy document supplied by the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  A remote &lt;code&gt;PERMIT&lt;/code&gt; is not enough by itself
&lt;/h2&gt;

&lt;p&gt;The adapter calls a fixed HTTPS &lt;code&gt;/governance-engine&lt;/code&gt; endpoint with explicit connection, request, and decision timeouts. It also bounds the response size and requires an exact JSON schema.&lt;/p&gt;

&lt;p&gt;An allow result requires all of the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;policy status is OKAY
AND decision is PERMIT
AND authorised is true
AND no unfulfilled obligatory statement exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing, contradictory, malformed, oversized, timed-out, cancelled, or unavailable responses deny. Unknown JSON fields deny. A &lt;code&gt;PERMIT&lt;/code&gt; paired with &lt;code&gt;authorised: false&lt;/code&gt; denies. Obligations deny until a named handler is implemented and tested.&lt;/p&gt;

&lt;p&gt;TLS verification cannot be disabled, and the PingAuthorize hostname must match its runtime certificate. The policy package is mounted read-only and cannot be loaded from an arbitrary network location.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy tuple
&lt;/h2&gt;

&lt;p&gt;The repository-owned deployment package permits only an exact conjunction over the trusted values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logical agent
AND original workload
AND immediate caller
AND purpose
AND scope
AND target
AND tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for AI agent integrations because a broad statement such as "this agent may use MCP" is usually too weak. The useful decision is closer to "this approved agent workload, acting for this verified user transaction and purpose, may invoke this tool through this caller path."&lt;/p&gt;

&lt;h2&gt;
  
  
  OPA and PingAuthorize can share a contract
&lt;/h2&gt;

&lt;p&gt;The Go gateway has a small authorization interface. An in-process OPA adapter is the default local option, while PingAuthorize implements the same typed contract through its JSON PDP API.&lt;/p&gt;

&lt;p&gt;That makes the policy engine replaceable without moving identity validation into the policy layer. It also allows the same allow and deny matrix to be tested against both implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests prove
&lt;/h2&gt;

&lt;p&gt;Failure coverage includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;forged logical agent&lt;/li&gt;
&lt;li&gt;wrong workload or immediate caller&lt;/li&gt;
&lt;li&gt;missing or ambiguous scope&lt;/li&gt;
&lt;li&gt;malformed and oversized decisions&lt;/li&gt;
&lt;li&gt;policy errors and unavailable service&lt;/li&gt;
&lt;li&gt;unsafe endpoint or disabled TLS validation&lt;/li&gt;
&lt;li&gt;unfulfilled obligations&lt;/li&gt;
&lt;li&gt;cancellation and timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The local live test has also shown an exact tuple returning &lt;code&gt;PERMIT&lt;/code&gt; and a forged logical agent returning &lt;code&gt;NOT_APPLICABLE&lt;/code&gt; over certificate-validated HTTPS.&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="//03-spire-workload-identity.md"&gt;Binding a logical agent to a real workload with SPIRE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="//05-end-to-end-implementation.md"&gt;Building and proving the complete request path&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/darkedges/pf-tts" rel="noopener noreferrer"&gt;https://github.com/darkedges/pf-tts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pingauthorize</category>
      <category>security</category>
      <category>authorization</category>
      <category>ai</category>
    </item>
    <item>
      <title>Trusted AI Agent Transactions, Part 3: SPIRE Workload Identity</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:02:55 +0000</pubDate>
      <link>https://dev.to/darkedges/trusted-ai-agent-transactions-part-3-spire-workload-identity-1gfb</link>
      <guid>https://dev.to/darkedges/trusted-ai-agent-transactions-part-3-spire-workload-identity-1gfb</guid>
      <description>&lt;h1&gt;
  
  
  Binding a logical agent to a real workload with SPIRE
&lt;/h1&gt;

&lt;p&gt;&lt;a href="//02-pingfederate-token-exchange.md"&gt;Part 2&lt;/a&gt; used a SPIRE JWT-SVID as the actor token in PingFederate token exchange. That proves more than a caller-supplied agent name, but it helps to understand exactly what it proves.&lt;/p&gt;

&lt;p&gt;A logical agent describes an approved application role. A SPIFFE ID identifies an attested running workload. They are related, but they are not interchangeable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AgentID:  urn:agent:customer-support
SPIFFEID: spiffe://example.org/agent/customer-support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is a policy identity. The second is a cryptographically attested runtime identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two SVID formats, two jobs
&lt;/h2&gt;

&lt;p&gt;The implementation uses both SPIRE credential formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT-SVID authenticates the agent to PingFederate during RFC 8693 token exchange&lt;/li&gt;
&lt;li&gt;X.509-SVID authenticates workload-to-workload connections with mTLS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JWT-SVID uses an audience dedicated to the PingFederate Transaction Token Service integration. The Txn-Token audience identifies the trust domain in which the call chain is valid. The protected target and tool belong in transaction context and authorization policy. Reusing the actor-token audience for the Txn-Token would blur two distinct trust boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Immediate caller identity changes at every hop
&lt;/h2&gt;

&lt;p&gt;The transaction token remains unchanged through the request path, but the immediate caller changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent -&amp;gt; Gateway:     caller is the agent SPIFFE ID
Gateway -&amp;gt; MCP:       caller is the gateway SPIFFE ID
MCP -&amp;gt; Protected API: caller is the MCP server SPIFFE ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the protected API reject a direct agent call even if the agent possesses a valid transaction token. The API can require the verified transaction context and the expected MCP server identity on the current mTLS connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development without making lab shortcuts production policy
&lt;/h2&gt;

&lt;p&gt;The Docker lab runs a SPIRE Server and Agent and uses Docker workload attestation. Each workload has a distinct label and registration entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wai.workload=demo-agent
wai.workload=mcp-gateway
wai.workload=demo-mcp-server
wai.workload=demo-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those selectors produce separate SPIFFE IDs. Mounting the Workload API socket does not let a container choose an identity. SPIRE observes the calling workload and matches its external selectors to a registered entry.&lt;/p&gt;

&lt;p&gt;The lab uses a join token to bootstrap the SPIRE Agent. That is explicitly a development choice. A production deployment should use a node attestor appropriate to its environment, such as cloud instance identity, Kubernetes PSAT, X.509 proof of possession, or hardware-backed attestation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation and ambiguity
&lt;/h2&gt;

&lt;p&gt;SPIRE signing keys can overlap during normal rotation. The integration accepts multiple JWT authorities only when every entry is explicitly a JWT-SVID authority, uses a unique non-empty key ID, and satisfies the configured key and algorithm constraints.&lt;/p&gt;

&lt;p&gt;Unknown key IDs, duplicate IDs, unexpected algorithms, ambiguous identities, missing audiences, and conflicting claims fail closed. The token header is never allowed to choose a newly trusted algorithm dynamically.&lt;/p&gt;

&lt;p&gt;The binding rule remains the same throughout: a workload cannot become another logical agent by changing request data.&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="//02-pingfederate-token-exchange.md"&gt;Using PingFederate token exchange as the delegation boundary&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="//04-pingauthorize-policy.md"&gt;Making policy decisions with PingAuthorize&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/darkedges/pf-tts" rel="noopener noreferrer"&gt;https://github.com/darkedges/pf-tts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spiffe</category>
      <category>spire</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Trusted AI Agent Transactions, Part 2: PingFederate Token Exchange</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:01:49 +0000</pubDate>
      <link>https://dev.to/darkedges/trusted-ai-agent-transactions-part-2-pingfederate-token-exchange-35pn</link>
      <guid>https://dev.to/darkedges/trusted-ai-agent-transactions-part-2-pingfederate-token-exchange-35pn</guid>
      <description>&lt;h2&gt;
  
  
  Using PingFederate as a Transaction Token Service
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//01-the-identity-problem.md"&gt;Part 1&lt;/a&gt; separated the user, logical agent, runtime workload, agent execution, and transaction identities. This part shows where they are joined safely.&lt;/p&gt;

&lt;p&gt;In the Tokenetes architecture, a Transaction Token Service, or TTS, uses the RFC 8693 token exchange protocol to mint a Txn-Token. This implementation assigns that logical role to PingFederate.&lt;/p&gt;

&lt;p&gt;The agent sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subject_token      = the user's access token
subject_token_type = access token
actor_token        = the agent workload's JWT-SVID
actor_token_type   = JWT
requested_token_type = urn:ietf:params:oauth:token-type:txn_token
audience           = the transaction trust domain
scope              = the narrow purpose of this transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subject token answers who authorized the external invocation. The SPIRE actor token is an agent-specific extension that proves which attested workload is requesting the exchange. Neither token is accepted as a substitute for the other. The base Transaction Tokens profile relies on authenticated TTS clients; this design additionally carries explicit actor evidence so PingFederate can bind a logical AI agent to its runtime workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example subject and actor tokens
&lt;/h2&gt;

&lt;p&gt;The following payloads are illustrative decoded claims. In the implementation, claims are used only after the complete token has passed signature, issuer, audience, algorithm, key ID, and time validation. Raw tokens are never written to application or audit logs.&lt;/p&gt;

&lt;p&gt;The subject token is the user's OAuth access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://pingfederate.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wai-agent-token-exchange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid profile mcp:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wai-browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1770000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1770000300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jti"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subject-token-id"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It represents the authenticated user. It does not prove which agent workload is presenting it, and it is not forwarded to the MCP gateway or downstream services.&lt;/p&gt;

&lt;p&gt;The actor token is a JWT-SVID issued by SPIRE to the running agent workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://spire.example.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://example.org/agent/demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"urn:pingfederate:wai:token-exchange"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1770000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1770000060&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It proves the SPIFFE workload identity to PingFederate. It does not identify the user, and it does not let the workload choose a logical &lt;code&gt;AgentID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent submits both tokens to PingFederate using a form-encoded RFC 8693 request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/as/token.oauth2&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingfederate.example&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/x-www-form-urlencoded&lt;/span&gt;

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&amp;amp;subject_token=REDACTED_USER_ACCESS_TOKEN
&amp;amp;subject_token_type=urn:ietf:params:oauth:token-type:access_token
&amp;amp;actor_token=REDACTED_SPIRE_JWT_SVID
&amp;amp;actor_token_type=urn:ietf:params:oauth:token-type:jwt
&amp;amp;requested_token_type=urn:ietf:params:oauth:token-type:txn_token
&amp;amp;audience=example.org
&amp;amp;scope=mcp:invoke
&amp;amp;request_details=%7B%22target%22%3A%22mcp-gateway%22%2C%22tool%22%3A%22customer.read%22%7D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values are redacted here intentionally. Tokens belong in the protected request body over validated TLS, never in a URL, log message, audit event, or error response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PingFederate validates
&lt;/h2&gt;

&lt;p&gt;The PingFederate Token Exchange Processor Policy validates both sides of the delegation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user token processor validates the subject token&lt;/li&gt;
&lt;li&gt;the SPIRE processor validates the JWT-SVID signature, issuer, audience, time, key ID, and algorithm&lt;/li&gt;
&lt;li&gt;trusted configuration maps the verified SPIFFE ID to a logical &lt;code&gt;AgentID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the policy issues a small, short-lived transaction JWT for the requested audience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The caller never supplies an authoritative logical agent identity. A workload such as &lt;code&gt;spiffe://example.org/agent/demo&lt;/code&gt; is mapped to a configured logical agent such as &lt;code&gt;urn:agent:demo&lt;/code&gt; inside the trusted policy boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result is a Txn-Token
&lt;/h2&gt;

&lt;p&gt;After validating the subject and actor independently, PingFederate should issue a short-lived signed JWT conforming to the Transaction Tokens profile. Its protected header identifies it as a Txn-Token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"txntoken+jwt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pingfederate-transaction-signing-key"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body carries the immutable transaction context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://pingfederate.example/transaction-token-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"txn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"019..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"req_wl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://example.org/agent/demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tctx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer.read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:agent:demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instance_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"019..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://example.org/agent/demo"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rctx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:ietf:rfc:6749"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response uses the normal OAuth token response shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REDACTED_TRANSACTION_JWT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issued_token_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:ietf:params:oauth:token-type:txn_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"N_A"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Txn-Token is not sent in the OAuth &lt;code&gt;Authorization&lt;/code&gt; header between internal workloads. It uses its dedicated header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Txn-Token: REDACTED_TRANSACTION_JWT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is not replay resistant, so TLS, its very short lifetime, trust-domain audience validation, and immediate-caller SPIFFE mTLS are all required. The same Txn-Token remains unchanged through the MCP gateway, MCP server, and protected API. Each service validates it before using any decoded claim.&lt;/p&gt;

&lt;p&gt;Arbitrary claims are not copied from either input token. The original user access token and actor JWT-SVID stop at the exchange boundary and are not propagated to MCP servers or downstream APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current implementation status
&lt;/h2&gt;

&lt;p&gt;The repository already implements the difficult security boundaries: RFC 8693 exchange, independent subject and actor validation, workload-to-agent binding, short lifetime, immutable propagation, strict issuer and audience checks, SPIFFE mTLS at each hop, target authorization, and correlated audit.&lt;/p&gt;

&lt;p&gt;It currently represents the result with application-specific transaction claims and bearer-token transport. To claim Tokenetes-style Transaction Tokens conformance, it still needs to emit &lt;code&gt;typ: txntoken+jwt&lt;/code&gt;, request and return the Transaction Token type URN, map the context to &lt;code&gt;txn&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;tctx&lt;/code&gt;, &lt;code&gt;rctx&lt;/code&gt;, and &lt;code&gt;req_wl&lt;/code&gt;, use the trust domain as &lt;code&gt;aud&lt;/code&gt;, and propagate the JWT in the &lt;code&gt;Txn-Token&lt;/code&gt; header. Validation must reject the older application token shape once that migration is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform and plugin discovery
&lt;/h2&gt;

&lt;p&gt;The reference implementation manages PingFederate with the official Terraform provider. Terraform owns the token processors, token exchange policy, access token manager, mapping, scope, and OAuth client.&lt;/p&gt;

&lt;p&gt;Plugin field names are discovered from the running PingFederate version before configuration is generated. This avoids guessing descriptor fields or weakening validation when product versions differ.&lt;/p&gt;

&lt;p&gt;Startup and configuration are separate trust operations. The profile starts the pinned product and installs the tested custom plugin. Scope creation and Terraform apply remain explicit operations, so starting a container cannot silently rotate an OAuth client secret or mutate token policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean bootstrap without committed secrets
&lt;/h2&gt;

&lt;p&gt;The repository-owned profile overlay is mounted read-only. A local generator creates short-lived bootstrap TLS material, datastore credentials, and system keys with cryptographically secure randomness. Those values are written to an ignored local file and never committed.&lt;/p&gt;

&lt;p&gt;The clean-volume test then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;starts PingFederate with random test-owned names and ports&lt;/li&gt;
&lt;li&gt;validates the bootstrap certificate&lt;/li&gt;
&lt;li&gt;applies managed TLS first&lt;/li&gt;
&lt;li&gt;reconnects using the new trust anchor&lt;/li&gt;
&lt;li&gt;applies the complete Terraform configuration&lt;/li&gt;
&lt;li&gt;performs a live token exchange&lt;/li&gt;
&lt;li&gt;proves that a tampered actor token is rejected&lt;/li&gt;
&lt;li&gt;removes only the exact test-owned resources&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is important because a configuration that only works against a long-lived developer volume is not a reproducible security control.&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="//01-the-identity-problem.md"&gt;The identity problem behind AI agents&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="//03-spire-workload-identity.md"&gt;Binding a logical agent to a real workload with SPIRE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://www.cncf.io/projects/tokenetes/" rel="noopener noreferrer"&gt;CNCF Tokenetes&lt;/a&gt; and the &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-transaction-tokens/" rel="noopener noreferrer"&gt;IETF Transaction Tokens draft&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/darkedges/pf-tts" rel="noopener noreferrer"&gt;https://github.com/darkedges/pf-tts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pingfederate</category>
      <category>oauth</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Trusted AI Agent Transactions, Part 1: The Identity Problem</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:00:58 +0000</pubDate>
      <link>https://dev.to/darkedges/trusted-ai-agent-transactions-part-1-the-identity-problem-17aa</link>
      <guid>https://dev.to/darkedges/trusted-ai-agent-transactions-part-1-the-identity-problem-17aa</guid>
      <description>&lt;h2&gt;
  
  
  The identity problem behind AI agents
&lt;/h2&gt;

&lt;p&gt;An application that calls an API for a user normally answers one identity question: who is the user?&lt;/p&gt;

&lt;p&gt;An AI agent introduces several more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which user authorized the work?&lt;/li&gt;
&lt;li&gt;Which logical agent is acting?&lt;/li&gt;
&lt;li&gt;Which running workload actually made the call?&lt;/li&gt;
&lt;li&gt;Which agent execution is involved?&lt;/li&gt;
&lt;li&gt;Which transaction and purpose does the call belong to?&lt;/li&gt;
&lt;li&gt;Which service made the current network hop?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting every answer into a single &lt;code&gt;sub&lt;/code&gt; claim creates a dangerous illusion. A logical agent name is not cryptographic proof of the running process. A workload identity is not the user. A transaction identifier is not a caller identity.&lt;/p&gt;

&lt;p&gt;This implementation keeps five values separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UserID&lt;/code&gt;: the authenticated human or upstream principal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AgentID&lt;/code&gt;: the approved logical agent definition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AgentInstanceID&lt;/code&gt;: one execution of that agent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SPIFFEID&lt;/code&gt;: the attested runtime workload&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TransactionID&lt;/code&gt;: the immutable identifier for the delegated action&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a user access token is not enough
&lt;/h2&gt;

&lt;p&gt;Passing the original user token through every service has three problems.&lt;/p&gt;

&lt;p&gt;First, downstream services cannot reliably distinguish the user from the agent acting for the user. Second, a broadly scoped user token may reach systems that never needed it. Third, the token usually says little about the purpose of this specific transaction.&lt;/p&gt;

&lt;p&gt;The safer model is the Transaction Tokens architecture implemented by Tokenetes. An external endpoint exchanges the incoming authorization at a controlled Transaction Token Service boundary. The result is a short-lived signed Txn-Token containing immutable identity and authorization context for one call chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an &lt;code&gt;agent_id&lt;/code&gt; field is not proof
&lt;/h2&gt;

&lt;p&gt;An HTTP request can claim any agent name. If the server authorizes that value directly, one workload can impersonate another by changing a string.&lt;/p&gt;

&lt;p&gt;The logical &lt;code&gt;AgentID&lt;/code&gt; must instead come from trusted policy that maps a cryptographically verified workload identity to an approved agent. The caller can request an operation, but it cannot choose its identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Immutable context and authenticated hops
&lt;/h2&gt;

&lt;p&gt;The Txn-Token carries immutable delegated context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user + logical agent + original workload + transaction + purpose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each network connection independently proves the immediate caller through SPIFFE mTLS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verified transaction context
AND verified immediate caller
AND target policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is not rewritten at every hop. That avoids giving intermediate services the power to remove or forge earlier context. The same transaction ID can be correlated across the agent, gateway, MCP server, and protected API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User OAuth token
  -&amp;gt; AI agent
  -&amp;gt; PingFederate RFC 8693 token exchange
  -&amp;gt; short-lived transaction JWT
  -&amp;gt; MCP gateway
  -&amp;gt; MCP server
  -&amp;gt; protected API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this design, PingFederate fills the logical Transaction Token Service role. SPIRE supplies a JWT-SVID as agent workload evidence during exchange and X.509-SVIDs for mTLS between workloads. PingAuthorize evaluates the verified Txn-Token context at the gateway.&lt;/p&gt;

&lt;p&gt;Tokenetes is the reference point for the call-chain model: a Txn-Token is minted once for the external invocation, remains immutable as it moves downstream, and is accepted only inside its trust domain. The immediate network caller is still authenticated separately.&lt;/p&gt;

&lt;p&gt;The result is not a new identity system. It is a way to combine existing user, workload, delegation, transport, and policy controls without confusing their responsibilities.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="//02-pingfederate-token-exchange.md"&gt;Using PingFederate token exchange as the delegation boundary&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/darkedges/pf-tts" rel="noopener noreferrer"&gt;https://github.com/darkedges/pf-tts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>oauth</category>
      <category>ai</category>
      <category>pingidentity</category>
    </item>
  </channel>
</rss>
