<?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>VEX demo update: adding Docker Scout attestations (and three new gotchas)</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 13 Jun 2026 03:58:54 +0000</pubDate>
      <link>https://dev.to/darkedges/vex-demo-update-adding-docker-scout-attestations-and-three-new-gotchas-dk</link>
      <guid>https://dev.to/darkedges/vex-demo-update-adding-docker-scout-attestations-and-three-new-gotchas-dk</guid>
      <description>&lt;p&gt;This is a follow-up to&lt;br&gt;
&lt;a href="https://dev.to/darkedges/from-70-cves-to-0-a-hands-on-vex-suppression-workflow-with-trivy-and-a-path-to-wiz-5gb6"&gt;From 70 CVEs to 0: a hands-on VEX suppression workflow with Trivy&lt;/a&gt;.&lt;br&gt;
The original demo covered Trivy's VEX repository and the filesystem-embed&lt;br&gt;
approach. This update adds &lt;strong&gt;Docker Scout attestations&lt;/strong&gt; as a first-class&lt;br&gt;
distribution channel and surfaces three new gotchas that aren't in the docs.&lt;/p&gt;

&lt;p&gt;The demo repo is the same:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/darkedges/trivy-vex-demo" rel="noopener noreferrer"&gt;github.com/darkedges/trivy-vex-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pull the latest and the new steps are already in &lt;code&gt;run.sh&lt;/code&gt; / &lt;code&gt;run.ps1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;The demo now has &lt;strong&gt;three distribution channels&lt;/strong&gt; for the same OpenVEX statements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;channel&lt;/th&gt;
&lt;th&gt;mechanism&lt;/th&gt;
&lt;th&gt;consumed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker scout attestation add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker Scout (from registry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;VEX repository (spec v0.1)&lt;/td&gt;
&lt;td&gt;Trivy via &lt;code&gt;--vex repo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Wiz registry attestation&lt;/td&gt;
&lt;td&gt;Wiz (unchanged from original)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Channels 1 and 2 are independent; updating one does not update the other.&lt;br&gt;
They use &lt;strong&gt;different product PURLs&lt;/strong&gt; and different packaging, but start from&lt;br&gt;
the same &lt;code&gt;vexctl create&lt;/code&gt; output.&lt;/p&gt;


&lt;h2&gt;
  
  
  Gotcha 4: Docker Scout won't match &lt;code&gt;pkg:oci/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The original demo used a &lt;code&gt;pkg:oci&lt;/code&gt; purl pinned to the image digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkg:oci/pingaccess@sha256:51689e8c…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Scout ignores it. Scout expects the &lt;strong&gt;&lt;code&gt;pkg:docker/&lt;/code&gt;&lt;/strong&gt; form with the&lt;br&gt;
full org/name path and the tag as the version component, separated by &lt;code&gt;@&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkg:docker/pingidentity/pingaccess@8.3.4-edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the &lt;code&gt;generate-vex.sh&lt;/code&gt; script now runs &lt;strong&gt;two loops&lt;/strong&gt;: one producing&lt;br&gt;
&lt;code&gt;.openvex.json&lt;/code&gt; files with a &lt;code&gt;pkg:oci&lt;/code&gt; digest purl for Trivy/Wiz, and a&lt;br&gt;
second producing &lt;code&gt;.vex.json&lt;/code&gt; files with the &lt;code&gt;pkg:docker&lt;/code&gt; tag purl for Scout.&lt;br&gt;
Same CVE set, different product identifiers, different output directories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trivy/Wiz channel - digest-pinned&lt;/span&gt;
vexctl create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--product&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pkg:oci/pingaccess@sha256:51689e8ccf1ec6bef28c855a2f2fafdd3556f753609adad2e258580e3bc9397c"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vuln&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2022-46337"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vex/statements/CVE-2022-46337.openvex.json"&lt;/span&gt; ...

&lt;span class="c"&gt;# Scout channel - tag-based&lt;/span&gt;
vexctl create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--product&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pkg:docker/pingidentity/pingaccess@8.3.4-edge"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vuln&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2022-46337"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vex/statements-scout/CVE-2022-46337.vex.json"&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dry-run check before attaching anything is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker scout cves pingidentity/pingaccess:8.3.4-edge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vex-location&lt;/span&gt; ./vex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;not_affected&lt;/code&gt; CVEs still appear, the first thing to check is the PURL&lt;br&gt;
prefix - &lt;code&gt;pkg:oci/&lt;/code&gt; in a Scout context is a silent no-match.&lt;/p&gt;


&lt;h2&gt;
  
  
  Gotcha 5: attestations and filesystem VEX are mutually exclusive (Scout)
&lt;/h2&gt;

&lt;p&gt;In the original post I mentioned that Trivy doesn't auto-discover VEX&lt;br&gt;
embedded in the image filesystem. Scout has a related but different rule that&lt;br&gt;
is much easier to trigger accidentally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If an image has any attestation, Scout reads only attestations and ignores&lt;br&gt;
all filesystem-embedded VEX documents entirely.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Any attestation" includes the provenance and SBOM attestations that modern&lt;br&gt;
BuildKit adds automatically when you &lt;code&gt;docker build&lt;/code&gt; without explicitly opting&lt;br&gt;
out. So this build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-f&lt;/span&gt; embed/Dockerfile &lt;span class="nt"&gt;-t&lt;/span&gt; myimage:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…will silently have provenance attached (BuildKit default since Docker Desktop&lt;br&gt;
4.11), and Scout will never read the &lt;code&gt;*.vex.json&lt;/code&gt; files you &lt;code&gt;COPY&lt;/code&gt;d into&lt;br&gt;
&lt;code&gt;/usr/share/vex/&lt;/code&gt;. You won't get an error; Scout just returns unfiltered&lt;br&gt;
results.&lt;/p&gt;

&lt;p&gt;To actually use the filesystem path you must suppress both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--provenance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--sbom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; embed/Dockerfile &lt;span class="nt"&gt;-t&lt;/span&gt; myimage:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice: &lt;strong&gt;prefer attestations&lt;/strong&gt;. They travel with the image in the&lt;br&gt;
registry, require no extraction, and don't silently vanish because BuildKit&lt;br&gt;
added something. The filesystem path is useful when you can't push to a&lt;br&gt;
registry at all.&lt;/p&gt;


&lt;h2&gt;
  
  
  Gotcha 6: Scout attestations need the containerd image store
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;docker scout attestation add&lt;/code&gt; against a local &lt;code&gt;registry:2&lt;/code&gt; requires&lt;br&gt;
the &lt;strong&gt;containerd image store&lt;/strong&gt; to be active in your Docker daemon. With the&lt;br&gt;
default overlay2 store the command may succeed but Scout won't resolve the&lt;br&gt;
attestation manifest correctly when reading it back.&lt;/p&gt;

&lt;p&gt;Check with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'containerd-snapshotter'&lt;/span&gt;
&lt;span class="c"&gt;# containerd-snapshotter: true  ← you're good&lt;/span&gt;
&lt;span class="c"&gt;# (nothing)                     ← not enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To enable it, add to &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and restart Docker:&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;"features"&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;"containerd-snapshotter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Enabling the containerd image store can affect existing local images - they&lt;br&gt;
won't be visible in the new store until re-pulled. Enable it in a dev&lt;br&gt;
environment before hitting production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;run.sh&lt;/code&gt; / &lt;code&gt;run.ps1&lt;/code&gt; scripts now detect this automatically and &lt;strong&gt;skip&lt;br&gt;
steps 9–11&lt;/strong&gt; (the Scout attestation flow) rather than blocking, so the rest&lt;br&gt;
of the demo - Trivy VEX repo, filesystem fallback, suppression proofs - runs&lt;br&gt;
uninterrupted regardless of your daemon config.&lt;/p&gt;


&lt;h2&gt;
  
  
  The attestation flow end-to-end
&lt;/h2&gt;

&lt;p&gt;Once the containerd store is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Verify suppression locally before attaching anything&lt;/span&gt;
docker scout cves pingidentity/pingaccess:8.3.4-edge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vex-location&lt;/span&gt; ./vex

&lt;span class="c"&gt;# 2. Push to a local registry:2 (no public push)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;--name&lt;/span&gt; vex-registry registry:2
docker tag pingidentity/pingaccess:8.3.4-edge &lt;span class="se"&gt;\&lt;/span&gt;
  localhost:5000/pingidentity/pingaccess:8.3.4-edge
docker push localhost:5000/pingidentity/pingaccess:8.3.4-edge

&lt;span class="c"&gt;# 3. Attach each VEX document as an in-toto attestation&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;vex/statements-scout/&lt;span class="k"&gt;*&lt;/span&gt;.vex.json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;docker scout attestation add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--predicate-type&lt;/span&gt; https://openvex.dev/ns/v0.2.0 &lt;span class="se"&gt;\&lt;/span&gt;
    localhost:5000/pingidentity/pingaccess:8.3.4-edge
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# 4. Re-scan to prove suppression&lt;/span&gt;
docker scout cves localhost:5000/pingidentity/pingaccess:8.3.4-edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few operational notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attestations &lt;strong&gt;cannot be removed&lt;/strong&gt; once attached; re-attaching a new
document for the same CVE overwrites it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker scout attestation add&lt;/code&gt; works without a rebuild - the image is
already in the registry.&lt;/li&gt;
&lt;li&gt;A plain &lt;code&gt;registry:2&lt;/code&gt; supports OCI artifact manifests (used by attestations)
from Docker Registry API v2.9 onward; if yours is older you'll need to
upgrade or fall back to the filesystem path.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Updated toolchain image
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;toolchain/Dockerfile&lt;/code&gt; now includes &lt;code&gt;docker-cli&lt;/code&gt; and the Docker Scout&lt;br&gt;
plugin installed via the official &lt;code&gt;install.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; ca-certificates curl jq bash docker-cli

&lt;span class="c"&gt;# Docker Scout CLI - verify latest at https://docs.docker.com/scout/install/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the toolchain container run Scout commands when the Docker socket is&lt;br&gt;
mounted, as a self-contained alternative to installing Scout on the host.&lt;/p&gt;


&lt;h2&gt;
  
  
  Summary: which mechanism to use when
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;you want to…&lt;/th&gt;
&lt;th&gt;use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Suppress in Trivy, no registry push&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--vex &amp;lt;file&amp;gt;&lt;/code&gt; or &lt;code&gt;--vex repo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suppress in Scout, image already in registry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker scout attestation add&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suppress in Wiz&lt;/td&gt;
&lt;td&gt;registry attestation (same Scout command, public registry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embed for distribution (consumers extract + &lt;code&gt;--vex&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COPY&lt;/code&gt; into image, build with &lt;code&gt;--provenance=false --sbom=false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full demo still runs in one command and nothing leaves the host (local&lt;br&gt;
&lt;code&gt;registry:2&lt;/code&gt; stands in for a real registry):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./run.sh        &lt;span class="c"&gt;# pauses after each step&lt;/span&gt;
./run.sh &lt;span class="nt"&gt;-y&lt;/span&gt;     &lt;span class="c"&gt;# unattended&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Questions on PURL matching, CycloneDX VEX, or Grype's behaviour with any of&lt;br&gt;
this? Drop them in the comments.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From 70 CVEs to 0: a hands-on VEX suppression workflow with Trivy (and a path to Wiz)</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 13 Jun 2026 00:30:16 +0000</pubDate>
      <link>https://dev.to/darkedges/from-70-cves-to-0-a-hands-on-vex-suppression-workflow-with-trivy-and-a-path-to-wiz-5gb6</link>
      <guid>https://dev.to/darkedges/from-70-cves-to-0-a-hands-on-vex-suppression-workflow-with-trivy-and-a-path-to-wiz-5gb6</guid>
      <description>&lt;p&gt;Run Trivy against almost any vendor container image and you'll get a wall of&lt;br&gt;
findings. Most of them don't matter, the vulnerable code path is never&lt;br&gt;
executed in your deployment. The problem is that &lt;em&gt;knowing&lt;/em&gt; that has&lt;br&gt;
traditionally lived in spreadsheets and Jira comments, where no scanner can&lt;br&gt;
see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VEX (Vulnerability Exploitability eXchange)&lt;/strong&gt; turns that triage into&lt;br&gt;
machine-readable statements that scanners apply automatically. This post&lt;br&gt;
walks through a complete, runnable workflow, baseline scan → OpenVEX&lt;br&gt;
generation → a hosted VEX repository → suppressed re-scan, and covers three&lt;br&gt;
gotchas I hit that the docs don't mention.&lt;/p&gt;

&lt;p&gt;Everything here is in a one-script demo repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/darkedges/trivy-vex-demo" rel="noopener noreferrer"&gt;github.com/darkedges/trivy-vex-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ The demo marks every CVE &lt;code&gt;not_affected&lt;/code&gt; mechanically to exercise the&lt;br&gt;
&lt;em&gt;plumbing&lt;/em&gt;. In real life the assessment is the valuable part, don't ship&lt;br&gt;
VEX statements you can't defend.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Target: &lt;code&gt;pingidentity/pingaccess:8.3.4-edge&lt;/code&gt; (digest&lt;br&gt;
&lt;code&gt;sha256:51689e8c…&lt;/code&gt;). Tools, pinned and current at time of writing: Trivy&lt;br&gt;
v0.71.0, vexctl v0.4.1, OpenVEX v0.2.0, and the&lt;br&gt;
&lt;a href="https://github.com/aquasecurity/vex-repo-spec" rel="noopener noreferrer"&gt;VEX Repository Specification&lt;/a&gt; v0.1.&lt;/p&gt;

&lt;p&gt;A small Alpine-based toolchain image carries everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:latest&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; TRIVY_VERSION=0.71.0&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; VEXCTL_VERSION=0.4.1&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk upgrade &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; ca-certificates curl jq
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/trivy.tar.gz &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="s2"&gt;"https://github.com/aquasecurity/trivy/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIVY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/trivy_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIVY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_Linux-64bit.tar.gz"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/trivy.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /usr/local/bin trivy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/trivy.tar.gz
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/bin/vexctl &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="s2"&gt;"https://github.com/openvex/vexctl/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VEXCTL_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/vexctl-linux-amd64"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/vexctl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1, Baseline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--output&lt;/span&gt; baseline-report.json &lt;span class="se"&gt;\&lt;/span&gt;
  pingidentity/pingaccess:8.3.4-edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;70 findings (2 CRITICAL / 18 HIGH / 42 MEDIUM / 8 LOW)&lt;/strong&gt; across 51&lt;br&gt;
unique CVE/GHSA IDs, all in bundled Java jars and git-lfs. The Alpine OS&lt;br&gt;
layer itself: zero.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2, Generate OpenVEX statements
&lt;/h2&gt;

&lt;p&gt;One statement per CVE with &lt;code&gt;vexctl&lt;/code&gt;. The critical detail is the &lt;strong&gt;product&lt;br&gt;
identifier&lt;/strong&gt;: a &lt;code&gt;pkg:oci&lt;/code&gt; purl pinned to the image &lt;strong&gt;digest&lt;/strong&gt;, never the tag,&lt;br&gt;
tags move, VEX assertions shouldn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vexctl create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--product&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pkg:oci/pingaccess@sha256:51689e8ccf1ec6bef28c855a2f2fafdd3556f753609adad2e258580e3bc9397c"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vuln&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2022-46337"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"not_affected"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--justification&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vulnerable_code_not_in_execute_path"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--status-note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"DEMO assessment for a suppression-workflow POC."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"DarkEdges Security &amp;lt;nirving@darkedges.com&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2022-46337.openvex.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which produces:&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;"@context"&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://openvex.dev/ns/v0.2.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;"@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;"https://darkedges.com/vex/demo/pingaccess/CVE-2022-46337"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DarkEdges Security &amp;lt;nirving@darkedges.com&amp;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;"statements"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"vulnerability"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-2022-46337"&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;"products"&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="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;"pkg:oci/pingaccess@sha256:51689e8c…"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_affected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"justification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vulnerable_code_not_in_execute_path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status_notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEMO assessment for a suppression-workflow POC."&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="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;Loop that over the 51 IDs from the baseline JSON, then consolidate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vexctl merge statements/&lt;span class="k"&gt;*&lt;/span&gt;.openvex.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pingaccess-8.3.4-edge.openvex.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: leave the purl &lt;strong&gt;qualifiers off&lt;/strong&gt; in statement products. Per Trivy's&lt;br&gt;
matching rules, a purl without qualifiers matches regardless of &lt;code&gt;arch&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;repository_url&lt;/code&gt;; with qualifiers, you're signing up for exact matching.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3, The instant win: &lt;code&gt;--vex&lt;/code&gt; flag
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image &lt;span class="nt"&gt;--show-suppressed&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vex&lt;/span&gt; pingaccess-8.3.4-edge.openvex.json &lt;span class="se"&gt;\&lt;/span&gt;
  pingidentity/pingaccess:8.3.4-edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;70 findings → 0 reported, 70 suppressed.&lt;/strong&gt; Each suppression is recorded in&lt;br&gt;
the JSON report under &lt;code&gt;Results[].ExperimentalModifiedFindings&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;Type: ignored&lt;/code&gt; and the justification, auditable, not deleted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4, Publish via a VEX repository
&lt;/h2&gt;

&lt;p&gt;A VEX repository is just static files: a manifest at&lt;br&gt;
&lt;code&gt;/.well-known/vex-repository.json&lt;/code&gt; and a tar.gz archive containing an index&lt;br&gt;
plus the documents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vex-repo/                       # webroot
├── .well-known/vex-repository.json
└── v0.1/vex-data.tar.gz        # contains: index.json + pkg/oci/pingaccess/vex.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DarkEdges Demo VEX Repository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"versions"&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;"spec_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"locations"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://vex-server/v0.1/vex-data.tar.gz"&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;"update_interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1h"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;~/.trivy/vex/repository.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;darkedges-demo&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://vex-server&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;trivy vex repo download&lt;/code&gt; fetches it, and scanning with &lt;code&gt;--vex repo&lt;/code&gt;&lt;br&gt;
gives the same result: &lt;strong&gt;0 findings, 70 suppressed&lt;/strong&gt;, except now the&lt;br&gt;
statements are centrally hosted, versioned, and every Trivy in your org picks&lt;br&gt;
them up automatically on the manifest's update interval.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three gotchas
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. The repository index needs &lt;code&gt;repository_url&lt;/code&gt; for OCI purls
&lt;/h3&gt;

&lt;p&gt;This one cost me an hour. The vex-repo-spec says index &lt;code&gt;id&lt;/code&gt;s are purls&lt;br&gt;
&lt;em&gt;"without version and qualifiers"&lt;/em&gt;, so &lt;code&gt;pkg:oci/pingaccess&lt;/code&gt;, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No match. Silently.&lt;/strong&gt; Trivy's index lookup (&lt;code&gt;pkg/vex/repo.go&lt;/code&gt;) makes an&lt;br&gt;
exception for OCI purls and &lt;strong&gt;keeps the &lt;code&gt;repository_url&lt;/code&gt; qualifier&lt;/strong&gt; as part&lt;br&gt;
of the package identity. The index entry must be:&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;"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;"pkg:oci/pingaccess?repository_url=index.docker.io%2Fpingidentity%2Fpingaccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pkg/oci/pingaccess/vex.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openvex"&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;If your repo downloads fine but suppresses nothing, check this first.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Embedding VEX in the image does nothing (for scanning)
&lt;/h3&gt;

&lt;p&gt;Docker's Hardened Images article mentions copying VEX documents into the&lt;br&gt;
image at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; pingidentity/pingaccess:8.3.4-edge&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pingaccess-8.3.4-edge.openvex.json /usr/share/vex/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested it: &lt;strong&gt;Trivy does not auto-discover VEX from the image filesystem.&lt;/strong&gt;&lt;br&gt;
A plain scan of the derived image still reports all 70 findings. Embedding is&lt;br&gt;
a &lt;em&gt;distribution&lt;/em&gt; mechanism, the consumer must extract the file and pass it&lt;br&gt;
via &lt;code&gt;--vex&lt;/code&gt;. (What Trivy &lt;em&gt;can&lt;/em&gt; auto-discover is VEX attached as a signed OCI&lt;br&gt;
&lt;strong&gt;registry attestation&lt;/strong&gt;, &lt;code&gt;trivy image --vex oci&lt;/code&gt;, which requires pushing.)&lt;/p&gt;
&lt;h3&gt;
  
  
  3. VEX binds to the digest, derived images don't inherit it
&lt;/h3&gt;

&lt;p&gt;The statements identify the product by the base image's digest. My derived&lt;br&gt;
image (built locally, never pushed) has &lt;strong&gt;no repo digest at all&lt;/strong&gt;, so neither&lt;br&gt;
the local file nor the repository suppressed anything on it. Expected, but&lt;br&gt;
worth internalizing: &lt;strong&gt;issue VEX for the digest you actually ship&lt;/strong&gt;, and&lt;br&gt;
regenerate when it changes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mechanism&lt;/th&gt;
&lt;th&gt;original image (digest-pinned)&lt;/th&gt;
&lt;th&gt;derived image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--vex &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 70/70 suppressed&lt;/td&gt;
&lt;td&gt;❌ 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--vex repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ 70/70 suppressed&lt;/td&gt;
&lt;td&gt;❌ 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;embedded file alone&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;❌ 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  What about Wiz?
&lt;/h2&gt;

&lt;p&gt;Wiz ingests OpenVEX &lt;strong&gt;automatically, zero config&lt;/strong&gt;, but from &lt;strong&gt;registry&lt;br&gt;
attestations&lt;/strong&gt;, not files or repositories. The hand-off from this workflow is&lt;br&gt;
one command against the image in a Wiz-scanned registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker scout attestation add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; pingaccess-8.3.4-edge.openvex.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--predicate-type&lt;/span&gt; https://openvex.dev/ns/v0.2.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt;registry&amp;gt;/&amp;lt;org&amp;gt;/pingaccess:8.3.4-edge
&lt;span class="c"&gt;# or: cosign attest --type openvex --predicate &amp;lt;file&amp;gt; &amp;lt;image@digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same OpenVEX documents, two consumers: Trivy via the repository, Wiz via the&lt;br&gt;
attestation. That's the point of a standard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1of1njvygt6trvmj6ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1of1njvygt6trvmj6ec.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo runs the whole thing, toolchain build, baseline, 51 statements,&lt;br&gt;
hosted repository, all the re-scans, with one command and colored output&lt;br&gt;
(there's a GIF of the full run in the README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/darkedges/trivy-vex-demo
&lt;span class="nb"&gt;cd &lt;/span&gt;trivy-vex-demo
./run.sh        &lt;span class="c"&gt;# pauses after each step; ./run.sh -y to run unattended&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is pushed anywhere; the only network traffic is Docker Hub pulls and&lt;br&gt;
Trivy DB downloads. PowerShell users get an equivalent &lt;code&gt;run.ps1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've hit other VEX edge cases, CycloneDX VEX, CSAF, Grype's matching&lt;br&gt;
behaviour, I'd love to hear about them in the comments.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy Ping Identity Products on Kubernetes with a Single Operator</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Wed, 27 May 2026 12:42:32 +0000</pubDate>
      <link>https://dev.to/darkedges/deploy-ping-identity-products-on-kubernetes-with-a-single-operator-11fk</link>
      <guid>https://dev.to/darkedges/deploy-ping-identity-products-on-kubernetes-with-a-single-operator-11fk</guid>
      <description>&lt;p&gt;Running Ping Identity's product suite on Kubernetes typically means wrestling with the &lt;code&gt;ping-devops&lt;/code&gt; Helm chart directly, a large umbrella chart with hundreds of values, no shared defaults across products, and no native Kubernetes status model. The &lt;code&gt;pingone-operator&lt;/code&gt; wraps that chart in a set of purpose-built custom resources so you declare &lt;em&gt;what you want&lt;/em&gt; and the operator figures out the Helm details.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through how the operator works, how to install it, and how to get PingFederate (and the rest of the suite) running in a cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design in one paragraph
&lt;/h2&gt;

&lt;p&gt;The operator introduces six custom resource definitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PingEnvironment&lt;/code&gt;&lt;/strong&gt; shared configuration: tenant ID, tier (development / staging / production), base domain, and ingress defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PingFederate&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;PingDirectory&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;PingAccess&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;PingAuthorize&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;PingAuthorizePAP&lt;/code&gt;&lt;/strong&gt;, one CR per product, each referencing a &lt;code&gt;PingEnvironment&lt;/code&gt; via &lt;code&gt;spec.environmentRef&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Underneath, everything still maps to a single &lt;code&gt;ping-devops&lt;/code&gt; Helm release per environment. Products are enabled or disabled in that release based on which product CRs exist. You get independent Kubernetes API objects (separate RBAC, separate status, deploy or remove products without touching shared config) without needing separate Helm releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;1.26+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubectl&lt;/td&gt;
&lt;td&gt;matching cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Helm&lt;/td&gt;
&lt;td&gt;3.x (CLI, for install only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ping Identity DevOps account&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pingidentity.com/devops/devopsPrograms/devopsRegistration.html" rel="noopener noreferrer"&gt;register here&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You'll also need a &lt;code&gt;devops-secret&lt;/code&gt; in whatever namespace products will be deployed to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic devops-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; pingone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;PING_IDENTITY_ACCEPT_EULA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;PING_IDENTITY_DEVOPS_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-devops-user&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;PING_IDENTITY_DEVOPS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-devops-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installing the operator
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: from the OCI Helm chart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; pingone-operator &lt;span class="se"&gt;\&lt;/span&gt;
  oci://ghcr.io/darkedges/charts/pingone-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 0.1.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; pingone-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: from source (Docker Desktop)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/darkedges/pingone-operator
&lt;span class="nb"&gt;cd &lt;/span&gt;pingone-operator
make docker-desktop   &lt;span class="c"&gt;# builds image, loads it into Docker Desktop, deploys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, verify the operator is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; pingone-system
&lt;span class="c"&gt;# NAME                                                READY   STATUS    RESTARTS   AGE&lt;/span&gt;
&lt;span class="c"&gt;# pingone-operator-controller-manager-...             1/1     Running   0          30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the CRDs if they aren't already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# or: kubectl apply -f config/crd/bases/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating your first environment
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;PingEnvironment&lt;/code&gt; holds the shared config that all product CRs in the same namespace will inherit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# env-dev.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingEnvironment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myorg-dev&lt;/span&gt;           &lt;span class="c1"&gt;# Helm release name: myorg-dev-ping&lt;/span&gt;
  &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;             &lt;span class="c1"&gt;# development | staging | production&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev.myorg.example.com&lt;/span&gt; &lt;span class="c1"&gt;# base domain for auto-derived hostnames&lt;/span&gt;

  &lt;span class="c1"&gt;# Shared ingress: all product CRs inherit this unless they override it&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nginx.ingress.kubernetes.io/backend-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTPS"&lt;/span&gt;
      &lt;span class="na"&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letsencrypt-prod"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; env-dev.yaml
kubectl get pingenvironments &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# NAME      PHASE   AGE&lt;/span&gt;
&lt;span class="c"&gt;# env-dev   Ready   5s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Helm release is created yet, the environment is a coordination point, not a trigger by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding PingFederate
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;PingFederate&lt;/code&gt; CR that references the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pf-dev.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingFederate&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-pf&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;       &lt;span class="c1"&gt;# references the PingEnvironment above&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;13.0.2-edge"&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

  &lt;span class="na"&gt;engineIngress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;# hostname auto-derived: pf.dev.myorg.example.com&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pf-tls&lt;/span&gt;

  &lt;span class="na"&gt;adminIngress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;# hostname auto-derived: pf-admin.dev.myorg.example.com&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pf-admin-tls&lt;/span&gt;

  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getting-started/pingfederate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; pf-dev.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;PingFederate&lt;/code&gt; reconciler validates the CR and queues the &lt;code&gt;PingEnvironment&lt;/code&gt; reconciler. That reconciler discovers all product CRs pointing at &lt;code&gt;env-dev&lt;/code&gt;, builds the combined Helm values, and installs or upgrades the &lt;code&gt;myorg-dev-ping&lt;/code&gt; release.&lt;/p&gt;

&lt;p&gt;Watch it come up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pingfederates &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# NAME     ENVIRONMENT   PHASE   AGE&lt;/span&gt;
&lt;span class="c"&gt;# dev-pf   env-dev       Ready   90s&lt;/span&gt;

kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# NAME                                        READY   STATUS    AGE&lt;/span&gt;
&lt;span class="c"&gt;# myorg-dev-ping-pingfederate-engine-0        1/1     Running   80s&lt;/span&gt;
&lt;span class="c"&gt;# myorg-dev-ping-pingfederate-admin-0         1/1     Running   80s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hostnames are derived automatically
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;spec.domain&lt;/code&gt; is set on the &lt;code&gt;PingEnvironment&lt;/code&gt;, you don't need to spell out every hostname. The operator derives them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Auto-derived hostname&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PingFederate engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pf.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingFederate admin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pf-admin.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingDataConsole&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-console.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingAccess admin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pa-admin.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingAccess engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pa.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingAuthorize&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paz.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingAuthorizePAP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paz-pap.&amp;lt;domain&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Override any of them by setting &lt;code&gt;hostname&lt;/code&gt; inside the component's ingress block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;engineIngress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sso.myorg.example.com&lt;/span&gt;   &lt;span class="c1"&gt;# explicit override&lt;/span&gt;
  &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sso-tls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding more products
&lt;/h2&gt;

&lt;p&gt;Each product is its own CR. Add them in any order, the operator reconciles the full set each time any one of them changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  PingDirectory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingDirectory&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-pd&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;storageSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8Gi&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline/pingdirectory&lt;/span&gt;
    &lt;span class="na"&gt;userBaseDN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dc=myorg,dc=com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PingAuthorize (with a startup dependency on PingDirectory)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingAuthorize&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-paz&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paz-tls&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingDirectory&lt;/span&gt;
        &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ldaps&lt;/span&gt;
        &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paz-pap-integration/pingauthorize&lt;/span&gt;
    &lt;span class="na"&gt;serverProfileLayers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paz&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline/pingauthorize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PingAuthorizePAP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingAuthorizePAP&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-pap&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paz-pap-tls&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paz-pap-integration/pingauthorizepap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PingAccess (waits for PingFederate)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PingAccess&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-pa&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingone&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-dev&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8.1.0-edge"&lt;/span&gt;
  &lt;span class="na"&gt;adminIngress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pa-admin-tls&lt;/span&gt;
  &lt;span class="na"&gt;engineIngress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tlsSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pa-tls&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingFederate&lt;/span&gt;
        &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
        &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getting-started/pingaccess&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Startup ordering with &lt;code&gt;waitFor&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;container.waitFor&lt;/code&gt; block controls which services a container waits for before it starts. Under the hood this maps to the &lt;code&gt;WAIT_FOR&lt;/code&gt; env var in the &lt;code&gt;ping-devops&lt;/code&gt; Docker images.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingDirectory&lt;/span&gt;
      &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ldaps&lt;/span&gt;
      &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pingFederate&lt;/span&gt;
      &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logical application names like &lt;code&gt;pingDirectory&lt;/code&gt;, &lt;code&gt;pingFederateAdmin&lt;/code&gt;, &lt;code&gt;pingAccessEngine&lt;/code&gt; are resolved by the operator to the correct Helm sub-chart service names automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layered server profiles
&lt;/h2&gt;

&lt;p&gt;The operator supports Ping Identity's layered profile pattern. Use &lt;code&gt;serverProfileLayers&lt;/code&gt; to chain profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serverProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/myorg/ping-profiles.git&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;extensions/pingfederate&lt;/span&gt;
    &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline&lt;/span&gt;           &lt;span class="c1"&gt;# chains to the layer named "baseline"&lt;/span&gt;

  &lt;span class="na"&gt;serverProfileLayers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pingidentity/pingidentity-server-profiles.git&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline/pingfederate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operator translates these to the &lt;code&gt;SERVER_PROFILE_*&lt;/code&gt; and &lt;code&gt;SERVER_PROFILE_&amp;lt;LAYER&amp;gt;_*&lt;/code&gt; env var convention the Docker images expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource sizing with &lt;code&gt;tier&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;tier&lt;/code&gt; once on the &lt;code&gt;PingEnvironment&lt;/code&gt; and the operator applies appropriate CPU/memory requests and limits to every product:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;PingFederate&lt;/th&gt;
&lt;th&gt;PingDirectory&lt;/th&gt;
&lt;th&gt;PingAccess&lt;/th&gt;
&lt;th&gt;PingAuthorize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;development&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500m / 512Mi&lt;/td&gt;
&lt;td&gt;500m / 1Gi&lt;/td&gt;
&lt;td&gt;500m / 512Mi&lt;/td&gt;
&lt;td&gt;500m / 1Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;staging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 1Gi&lt;/td&gt;
&lt;td&gt;1 / 2Gi&lt;/td&gt;
&lt;td&gt;1 / 1Gi&lt;/td&gt;
&lt;td&gt;1 / 2Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;production&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 / 2Gi&lt;/td&gt;
&lt;td&gt;2 / 4Gi&lt;/td&gt;
&lt;td&gt;2 / 2Gi&lt;/td&gt;
&lt;td&gt;2 / 4Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Checking status
&lt;/h2&gt;

&lt;p&gt;Each resource has its own &lt;code&gt;Phase&lt;/code&gt; field and standard Kubernetes conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pingenvironments,pingfederates,pingdirectories &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# NAME                               PHASE   AGE&lt;/span&gt;
&lt;span class="c"&gt;# pingenvironment.pingone.io/env-dev Ready   10m&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# NAME                               ENVIRONMENT   PHASE   AGE&lt;/span&gt;
&lt;span class="c"&gt;# pingfederate.pingone.io/dev-pf     env-dev       Ready   8m&lt;/span&gt;
&lt;span class="c"&gt;# pingdirectory.pingone.io/dev-pd    env-dev       Ready   7m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drill into conditions with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pingfederate dev-pf &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Removing a product
&lt;/h2&gt;

&lt;p&gt;Delete the product CR. The operator detects the deletion, rebuilds Helm values with that product disabled, and upgrades the release in place. The other products keep running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pingaccess dev-pa &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# PingAccess pods terminate; PingFederate and PingDirectory are unaffected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the &lt;code&gt;PingEnvironment&lt;/code&gt; to tear down the entire Helm release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pingenvironment env-dev &lt;span class="nt"&gt;-n&lt;/span&gt; pingone
&lt;span class="c"&gt;# Finalizer triggers Helm uninstall; all product pods are removed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The source is at &lt;a href="https://github.com/darkedges/pingone-kubernetes-provider" rel="noopener noreferrer"&gt;https://github.com/darkedges/pingone-kubernetes-provider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;examples/getting-started/&lt;/code&gt; directory has a complete working example for Docker Desktop with nginx ingress and cert-manager self-signed TLS&lt;/li&gt;
&lt;li&gt;Full field reference is in &lt;code&gt;CONFIGURATION.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>pingidentity</category>
      <category>operator</category>
      <category>helm</category>
    </item>
    <item>
      <title>From Laptop to Cluster: Running darkedges-entraid-tokenexchange with Docker and Kubernetes</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Wed, 29 Apr 2026 23:38:03 +0000</pubDate>
      <link>https://dev.to/darkedges/from-laptop-to-cluster-running-darkedges-entraid-tokenexchange-with-docker-and-kubernetes-43h4</link>
      <guid>https://dev.to/darkedges/from-laptop-to-cluster-running-darkedges-entraid-tokenexchange-with-docker-and-kubernetes-43h4</guid>
      <description>&lt;p&gt;I wanted one clean deployment path for local testing and real cluster rollout, so I packaged the same app for both Docker Compose and Kubernetes (Helm).&lt;/p&gt;

&lt;p&gt;This post is the exact runbook I now use for &lt;code&gt;darkedges-entraid-tokenexchange&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this setup works
&lt;/h2&gt;

&lt;p&gt;This app is a Next.js token-exchange broker with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OIDC sign-in (single or multiple providers)&lt;/li&gt;
&lt;li&gt;Redis-backed sessions&lt;/li&gt;
&lt;li&gt;Entra token exchange support&lt;/li&gt;
&lt;li&gt;Optional Vault integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is simple: run the same container image everywhere, then swap environment values per environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local path: Docker Compose
&lt;/h2&gt;

&lt;p&gt;The repository already includes a ready-to-run &lt;code&gt;docker-compose.yaml&lt;/code&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app&lt;/code&gt; on port &lt;code&gt;3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vault&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vault-init&lt;/code&gt; and &lt;code&gt;vault-data-init&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create your environment file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Populate at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXTAUTH_SECRET=&amp;lt;random-32-byte-secret&amp;gt;
NEXTAUTH_URL=http://localhost:3000

ENTRAID_TENANT_ID=&amp;lt;tenant-guid&amp;gt;
ENTRAID_CLIENT_ID=&amp;lt;app-client-id&amp;gt;
ENTRAID_CLIENT_SECRET=&amp;lt;app-client-secret&amp;gt;
ENTRAID_SCOPE=https://graph.microsoft.com/.default

REDIS_URL=redis://localhost:6379
VAULT_ADDR=http://localhost:18200
VAULT_TOKEN=dev-root-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you support multiple providers, also set &lt;code&gt;OIDC_PROVIDERS&lt;/code&gt; as valid JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  OIDC_PROVIDERS: quick configuration pattern
&lt;/h3&gt;

&lt;p&gt;Use a JSON array. Each provider entry needs the same core fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OIDC_PROVIDERS='[
  {
    "id": "pingfederate",
    "name": "PingFederate",
    "description": "Sign in with corporate credentials",
    "icon": "lock",
    "baseUrl": "https://id.example.com",
    "clientId": "your-client-id",
    "clientSecret": "your-client-secret",
    "scopes": ["openid", "profile", "email"]
  }
]'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For multiple providers, add more objects to the same array (for example Entra, PingFederate, Auth0).&lt;/p&gt;

&lt;p&gt;Tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep it valid JSON inside the quotes: double quotes for keys/strings, no trailing commas.&lt;/li&gt;
&lt;li&gt;Keep provider &lt;code&gt;id&lt;/code&gt; values unique.&lt;/li&gt;
&lt;li&gt;Make sure each &lt;code&gt;baseUrl&lt;/code&gt; and client credentials match the IdP app registration.&lt;/li&gt;
&lt;li&gt;Use each provider's correct callback URL in the IdP config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If login page/provider rendering fails, the first thing to check is JSON validity in &lt;code&gt;OIDC_PROVIDERS&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entra app types you should provision
&lt;/h3&gt;

&lt;p&gt;For this project, think in terms of three Entra app registrations (each with an Enterprise Application/service principal):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Web login app for OIDC sign-in (&lt;code&gt;msentraid&lt;/code&gt; provider in &lt;code&gt;OIDC_PROVIDERS&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;API/resource app exposing your delegated scope (for example &lt;code&gt;access_as_user&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Backend token-exchange app for OBO/Graph calls (&lt;code&gt;ENTRAID_CLIENT_ID&lt;/code&gt; / &lt;code&gt;ENTRAID_CLIENT_SECRET&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you support External ID (&lt;code&gt;ciamlogin.com&lt;/code&gt; issuer), add a fourth confidential app and map it to &lt;code&gt;ENTRAID_CIAM_*&lt;/code&gt; variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  1-minute env mapping check
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Key env vars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web login app&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OIDC_PROVIDERS[*].baseUrl&lt;/code&gt;, &lt;code&gt;OIDC_PROVIDERS[*].clientId&lt;/code&gt;, &lt;code&gt;OIDC_PROVIDERS[*].clientSecret&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API/resource app&lt;/td&gt;
&lt;td&gt;Usually represented in &lt;code&gt;NEXT_PUBLIC_ENTRAID_SCOPE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend token-exchange app&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ENTRAID_TENANT_ID&lt;/code&gt;, &lt;code&gt;ENTRAID_CLIENT_ID&lt;/code&gt;, &lt;code&gt;ENTRAID_CLIENT_SECRET&lt;/code&gt;, &lt;code&gt;ENTRAID_SCOPE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional CIAM app&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ENTRAID_CIAM_TENANT_ID&lt;/code&gt;, &lt;code&gt;ENTRAID_CIAM_CLIENT_ID&lt;/code&gt;, &lt;code&gt;ENTRAID_CIAM_CLIENT_SECRET&lt;/code&gt;, &lt;code&gt;ENTRAID_CIAM_SCOPE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Build and start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Verify runtime
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App: &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vault UI (optional): &lt;code&gt;http://localhost:18200&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Stop or reset
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full reset including volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cluster path: Kubernetes + Helm
&lt;/h2&gt;

&lt;p&gt;The chart is already in the repo at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;helm/darkedges-entraid-tokenexchange&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also an environment-flavored values file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;helm/broker.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Build and push image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;registry&amp;gt;/darkedges-entraid-tokenexchange:&amp;lt;tag&amp;gt; &lt;span class="nb"&gt;.&lt;/span&gt;
docker push &amp;lt;registry&amp;gt;/darkedges-entraid-tokenexchange:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create namespace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Add pull secret for private registry
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret docker-registry darkedges-registry-credentials &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;registry&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;username&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;password&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Update values
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;helm/broker.yaml&lt;/code&gt; (or your own values file), set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image.repository&lt;/code&gt; and &lt;code&gt;image.tag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingress.hosts&lt;/code&gt; and &lt;code&gt;ingress.tls&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;env&lt;/code&gt; for non-sensitive config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;secretEnv&lt;/code&gt; for secrets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REDIS_URL&lt;/code&gt; to a reachable Redis service&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Install or upgrade release
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; darkedges-entraid-tokenexchange &lt;span class="se"&gt;\&lt;/span&gt;
  helm/darkedges-entraid-tokenexchange &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; helm/broker.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; broker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Verify rollout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; broker
kubectl get svc &lt;span class="nt"&gt;-n&lt;/span&gt; broker
kubectl get ingress &lt;span class="nt"&gt;-n&lt;/span&gt; broker
kubectl rollout status deploy/darkedges-entraid-tokenexchange &lt;span class="nt"&gt;-n&lt;/span&gt; broker
kubectl logs deploy/darkedges-entraid-tokenexchange &lt;span class="nt"&gt;-n&lt;/span&gt; broker &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Access the app
&lt;/h3&gt;

&lt;p&gt;Preferred:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingress host over HTTPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quick test from laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward svc/darkedges-entraid-tokenexchange 3000:3000 &lt;span class="nt"&gt;-n&lt;/span&gt; broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;Three things made this reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep one container artifact and promote it across environments&lt;/li&gt;
&lt;li&gt;Keep non-secret and secret config split (&lt;code&gt;env&lt;/code&gt; vs &lt;code&gt;secretEnv&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Validate rollout every time with both status and logs, not just pod readiness&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;p&gt;Before go-live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move secrets from values files to a proper secret manager&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; to your real HTTPS URL&lt;/li&gt;
&lt;li&gt;Configure CPU and memory requests/limits&lt;/li&gt;
&lt;li&gt;Use managed or HA Redis&lt;/li&gt;
&lt;li&gt;Harden Vault TLS/auth and remove dev credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handy commands
&lt;/h2&gt;

&lt;p&gt;Upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade darkedges-entraid-tokenexchange &lt;span class="se"&gt;\&lt;/span&gt;
  helm/darkedges-entraid-tokenexchange &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; helm/broker.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm rollback darkedges-entraid-tokenexchange 1 &lt;span class="nt"&gt;-n&lt;/span&gt; broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uninstall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm uninstall darkedges-entraid-tokenexchange &lt;span class="nt"&gt;-n&lt;/span&gt; broker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already have Docker Compose and Helm in this repo, this workflow should get you from local proof-of-concept to repeatable Kubernetes deployment with minimal friction.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>kubernetes</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>PingFederate Token Exchange Processor Policy</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Mon, 20 Apr 2026 20:31:45 +0000</pubDate>
      <link>https://dev.to/darkedges/pingfederate-token-exchange-processor-policy-2h4e</link>
      <guid>https://dev.to/darkedges/pingfederate-token-exchange-processor-policy-2h4e</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Overview&lt;/li&gt;
&lt;li&gt;Complete Configuration Dependency Map&lt;/li&gt;
&lt;li&gt;PROCESSORPOLICIES Policy&lt;/li&gt;
&lt;li&gt;Processor Mapping 1 - PingFederate ↔ PingFederate&lt;/li&gt;
&lt;li&gt;Processor Mapping 2 - Microsoft Entra ID ↔ PingFederate&lt;/li&gt;
&lt;li&gt;Token Processor Comparison Matrix&lt;/li&gt;
&lt;li&gt;Access Token Mapping - Token Exchange Context&lt;/li&gt;
&lt;li&gt;OGNL Expression - Actor Claim Transformation&lt;/li&gt;
&lt;li&gt;AccessTokenManagement Configuration&lt;/li&gt;
&lt;li&gt;Real-World Example - HR Chatbot Token Exchange&lt;/li&gt;
&lt;li&gt;Token Lifecycle Sequence&lt;/li&gt;
&lt;li&gt;Configuration File Locations&lt;/li&gt;
&lt;li&gt;Validation Checklist&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;li&gt;References&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PROCESSORPOLICIES&lt;/code&gt; is the &lt;strong&gt;default&lt;/strong&gt; PingFederate Token Exchange Processor Policy implementing &lt;strong&gt;RFC 8693 delegation semantics&lt;/strong&gt;. When a client submits a token exchange request, this policy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Selects the correct &lt;strong&gt;processor mapping&lt;/strong&gt; based on the token types presented&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validates&lt;/strong&gt; both subject and actor tokens using the configured token processors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extracts claims&lt;/strong&gt; and maps them to a standardised attribute contract&lt;/li&gt;
&lt;li&gt;Passes the fulfilled attributes to the &lt;strong&gt;Access Token Mapping&lt;/strong&gt; which produces the final JWT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Supported exchange patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Subject Token Type&lt;/th&gt;
&lt;th&gt;Actor Token Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PingFederate ↔ PingFederate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:oauth:token-type:access_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:oauth:token-type:access_token&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Entra ID ↔ PingFederate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:oauth:token-type:access_token:msft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:oauth:token-type:access_token&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Semantic&lt;/strong&gt;: RFC 8693 &lt;strong&gt;delegation&lt;/strong&gt; - the actor (&lt;code&gt;contact-hr-client&lt;/code&gt;) acts on behalf of the subject (&lt;code&gt;user.1&lt;/code&gt;). The issued token contains an &lt;code&gt;actor&lt;/code&gt; claim (RFC 8693 §4.1) recording this explicitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Complete Configuration Dependency Map
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Token Exchange HTTP Request
POST /as/token.oauth2
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
│
├── subject_token      (user's access token)
├── subject_token_type
├── actor_token        (chatbot's client credentials token)
├── actor_token_type
├── client_id          (contact-oauth-client)
├── client_secret
└── scope
         │
         ▼
┌─────────────────────────────────────────────────┐
│  /oauth/tokenExchange/processor/settings        │
│  defaultProcessorPolicyRef: PROCESSORPOLICIES   │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────┐
│  /oauth/tokenExchange/processor/policies        │
│  PROCESSORPOLICIES                              │
│  actorTokenRequired: true                       │
│                                                 │
│  ┌─────────────────────────────────────────┐    │
│  │ Processor Mapping 1                     │    │
│  │ subjectTokenType: access_token (PF)     │    │
│  │ actorTokenType:   access_token (PF)     │    │
│  │ subjectProcessor: PFSubjectProcessor    │    │
│  │ actorProcessor:   PFActorSubject        │    │
│  └─────────────────────────────────────────┘    │
│                                                 │
│  ┌─────────────────────────────────────────┐    │
│  │ Processor Mapping 2                     │    │
│  │ subjectTokenType: access_token:msft     │    │
│  │ actorTokenType:   access_token (PF)     │    │
│  │ subjectProcessor: MSFTTOKENPROCESSOR    │    │
│  │ actorProcessor:   PFTOKENPROCESSOR      │    │
│  └─────────────────────────────────────────┘    │
└───────────────────┬─────────────────────────────┘
                    │
         ┌──────────┴──────────┐
         ▼                     ▼
  Token Processors         Token Processors
  (validate subject)       (validate actor)
  ┌───────────────┐        ┌────────────────┐
  │PFSubjectProc  │        │PFActorSubject  │
  │MSFTTOKENPROC  │        │PFTOKENPROCESSOR│
  └───────┬───────┘        └───────┬────────┘
          │                        │
          └──────────┬─────────────┘
                     │
                     ▼
         Attribute Contract Fulfillment
         (map claims from both tokens)
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  /oauth/accessTokenMappings                     │
│  Context: TOKEN_EXCHANGE_PROCESSOR_POLICY       │
│  Policy:  PROCESSORPOLICIES                     │
│  Manager: AccessTokenManagement                 │
│                                                 │
│  actor      ← OGNL expression (JSON object)     │
│  vaultloc.  ← TOKEN_EXCHANGE_PROCESSOR_POLICY   │
│  aud        ← CONTEXT (ClientId)                │
│  sub        ← TOKEN_EXCHANGE_PROCESSOR_POLICY   │
│  scope      ← TOKEN_EXCHANGE_PROCESSOR_POLICY   │
│  groups     ← TOKEN_EXCHANGE_PROCESSOR_POLICY   │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────┐
│  /oauth/accessTokenManagers                     │
│  AccessTokenManagement                          │
│                                                 │
│  Algorithm:  RS256                              │
│  Lifetime:   120 seconds                        │
│  SigningKey:  5jqt7j8mxbwl2awtpc465yzx1         │
│  Issuer:     https://id.example.com             │
│  Adds:  iss, iat, exp, jti                      │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
           Issued JWT Access Token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PROCESSORPOLICIES Policy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROCESSORPOLICIES&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROCESSORPOLICIES&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actorTokenRequired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; - actor token is &lt;strong&gt;mandatory&lt;/strong&gt; in all requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default Policy&lt;/td&gt;
&lt;td&gt;Yes - set as &lt;code&gt;defaultProcessorPolicyRef&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Attribute Contract
&lt;/h3&gt;

&lt;p&gt;The policy defines the attributes available for downstream mapping:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Source (Mapping 1)&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;sub&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;ACTOR_TOKEN → &lt;code&gt;client_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;vaultlocation&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;scope&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;groups&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;given_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;family_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;SUBJECT_TOKEN → &lt;code&gt;email&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Processor Mapping Selection Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request arrives with subject_token_type and actor_token_type
         │
         ├─ subject_token_type = urn:...:access_token   AND
         │  actor_token_type   = urn:...:access_token
         │        └─► Mapping 1 - PFSubjectProcessor + PFActorSubject
         │
         ├─ subject_token_type = urn:...:access_token:msft   AND
         │  actor_token_type   = urn:...:access_token
         │        └─► Mapping 2 - MSFTTOKENPROCESSOR + PFTOKENPROCESSOR
         │
         └─ No match
                  └─► HTTP 400 invalid_request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issuance Criteria
&lt;/h3&gt;

&lt;p&gt;Both mappings have &lt;strong&gt;empty&lt;/strong&gt; &lt;code&gt;conditionalCriteria&lt;/code&gt;:&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="nl"&gt;"issuanceCriteria"&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;"conditionalCriteria"&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="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;All token exchanges that pass processor validation are approved - no additional OGNL conditions are applied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Processor Mapping 1 - PingFederate ↔ PingFederate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;Used when both the subject and actor tokens were issued by &lt;strong&gt;this PingFederate instance&lt;/strong&gt; (&lt;code&gt;https://id.example.com&lt;/code&gt;). This is the primary path for the HR Chatbot integration.&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  ──► PFSubjectProcessor  (validates user's access token)
actor_token    ──► PFActorSubject      (validates chatbot's client credentials token)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PFSubjectProcessor
&lt;/h3&gt;

&lt;p&gt;Validates the &lt;strong&gt;user's access token&lt;/strong&gt; (subject token).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PFSubjectProcessor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.pingidentity.pf.tokenprocessors.jwt.JwtTokenProcessor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed Issuer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWKS URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com/pf/JWKS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Audience&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required Audience&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-hr-client&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Expiration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Issued At&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Skew&lt;/td&gt;
&lt;td&gt;0 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWKS Cache Duration&lt;/td&gt;
&lt;td&gt;720 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Attribute Contract Produced:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;sub&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;vaultlocation&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;scope&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;groups&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;given_name&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;family_name&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;email&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example subject token claims validated by this processor:&lt;/strong&gt;&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;"scope"&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="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_details"&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;"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;"contact-hr-client"&lt;/span&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://id.example.com"&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;1776667958&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;"xExadYEtur8OlKf9NW3pVI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vaultlocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"referenceid/msentraid/0BUTfOKfKbCi2Rf4S-krNcFQUAJ2R4YDIqf8Xvl5nK4"&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;"contact-hr-client"&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.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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="s2"&gt;"Administrators"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Seawell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxxx@hotmail.com"&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;1776675158&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;h3&gt;
  
  
  PFActorSubject
&lt;/h3&gt;

&lt;p&gt;Validates the &lt;strong&gt;chatbot's client credentials token&lt;/strong&gt; (actor token).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PFActorSubject&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.pingidentity.pf.tokenprocessors.jwt.JwtTokenProcessor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed Issuer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWKS URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com/pf/JWKS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Audience&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; - no audience check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Expiration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Issued At&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Skew&lt;/td&gt;
&lt;td&gt;0 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWKS Cache Duration&lt;/td&gt;
&lt;td&gt;720 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Attribute Contract Produced:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;sub&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;scope&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;client_id&lt;/code&gt; claim - &lt;strong&gt;used as &lt;code&gt;actor&lt;/code&gt; in output&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example actor token claims validated by this processor:&lt;/strong&gt;&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;"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;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_details"&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;"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;"contact-hr-client"&lt;/span&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://id.example.com"&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;1776667937&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;"ujPePxrAUOLlrj03ORmPe1"&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;1776675137&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The actor token has an empty &lt;code&gt;scope&lt;/code&gt; because it was obtained via Client Credentials grant - the chatbot is authenticating as itself, not as a user.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Attribute Fulfillment - Mapping 1
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output Attribute&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Input Claim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ACTOR_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Processor Mapping 2 - Microsoft Entra ID ↔ PingFederate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;Used when the subject token originated from &lt;strong&gt;Microsoft Entra ID / Azure AD&lt;/strong&gt; (identified by &lt;code&gt;subject_token_type: urn:ietf:params:oauth:token-type:access_token:msft&lt;/code&gt;), while the actor token is still a PingFederate bearer token.&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  ──► MSFTTOKENPROCESSOR  (validates Entra ID JWT)
actor_token    ──► PFTOKENPROCESSOR    (validates PF bearer token)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MSFTTOKENPROCESSOR
&lt;/h3&gt;

&lt;p&gt;Validates tokens issued by &lt;strong&gt;Microsoft Azure AD / Entra ID&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MSFTTOKENPROCESSOR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;com.pingidentity.pf.tokenprocessors.jwt.JwtTokenProcessor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tenant ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4161be3f-bf2b-41d4-a02b-e6f82b529d53&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Allowed Issuers:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issuer&lt;/th&gt;
&lt;th&gt;JWKS URL&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://sts.windows.net/4161be3f-bf2b-41d4-a02b-e6f82b529d53/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://login.microsoftonline.com/common/discovery/keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ADFS / v1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://login.microsoftonline.com/4161be3f-bf2b-41d4-a02b-e6f82b529d53/v2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://login.microsoftonline.com/4161be3f-bf2b-41d4-a02b-e6f82b529d53/discovery/v2.0/keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OIDC v2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Allowed Audiences:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Audience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://fram.connectid.darkedges.com/openam/oauth2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e83c2af3-43d1-4f62-8bff-e619c29b5026&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Require Audience&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Expiration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require Issued At&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Skew&lt;/td&gt;
&lt;td&gt;0 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Attribute Contract Produced:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;sub&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;JWT &lt;code&gt;email&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  PFTOKENPROCESSOR
&lt;/h3&gt;

&lt;p&gt;Validates &lt;strong&gt;PingFederate bearer access tokens&lt;/strong&gt; by introspecting against the &lt;code&gt;AccessTokenManagement&lt;/code&gt; token manager.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PFTOKENPROCESSOR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;org.sourceid.wstrust.processor.oauth.BearerAccessTokenTokenProcessor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access Token Manager&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AccessTokenManagement&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope as single string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Attribute Contract Produced (from AccessTokenManagement introspection):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token &lt;code&gt;aud&lt;/code&gt; claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authorization_details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token issuer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Token client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;Token subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extended&lt;/td&gt;
&lt;td&gt;Token email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Attribute Fulfillment - Mapping 2
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output Attribute&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Input Claim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(not included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUBJECT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Most attributes are &lt;code&gt;NO_MAPPING&lt;/code&gt; because Microsoft tokens do not contain PingFederate-specific claims such as &lt;code&gt;vaultlocation&lt;/code&gt; or &lt;code&gt;groups&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Token Processor Comparison Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;PFSubjectProcessor&lt;/th&gt;
&lt;th&gt;PFActorSubject&lt;/th&gt;
&lt;th&gt;MSFTTOKENPROCESSOR&lt;/th&gt;
&lt;th&gt;PFTOKENPROCESSOR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subject validator&lt;/td&gt;
&lt;td&gt;Actor validator&lt;/td&gt;
&lt;td&gt;Subject validator&lt;/td&gt;
&lt;td&gt;Actor validator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Token Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JWT (PF-issued)&lt;/td&gt;
&lt;td&gt;JWT (PF-issued)&lt;/td&gt;
&lt;td&gt;JWT (Azure-issued)&lt;/td&gt;
&lt;td&gt;Opaque Bearer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Issuer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Azure AD (v1 + v2)&lt;/td&gt;
&lt;td&gt;(any PF-issued)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWKS Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pf/JWKS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pf/JWKS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Azure Discovery&lt;/td&gt;
&lt;td&gt;AccessTokenManagement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Require Audience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Required Audience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-hr-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Azure app audiences&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Require Expiration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Require Issued At&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clock Skew&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0s&lt;/td&gt;
&lt;td&gt;0s&lt;/td&gt;
&lt;td&gt;0s&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key Claims Extracted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;groups&lt;/code&gt;, &lt;code&gt;vaultlocation&lt;/code&gt;, names, &lt;code&gt;email&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;client_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Access Token Mapping - Token Exchange Context
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mapping Identity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;urn:ietf:params:oauth:grant-type:token-exchange|PROCESSORPOLICIES|AccessTokenManagement&lt;/span&gt;
&lt;span class="na"&gt;Context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOKEN_EXCHANGE_PROCESSOR_POLICY → PROCESSORPOLICIES&lt;/span&gt;
&lt;span class="na"&gt;Manager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AccessTokenManagement&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Attribute Sources
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output Claim&lt;/th&gt;
&lt;th&gt;Source Type&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EXPRESSION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OGNL - builds JSON object &lt;code&gt;{ "sub": tepp.actor }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TOKEN_EXCHANGE_PROCESSOR_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CONTEXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ClientId&lt;/code&gt; - the requesting client ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TOKEN_EXCHANGE_PROCESSOR_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TOKEN_EXCHANGE_PROCESSOR_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TOKEN_EXCHANGE_PROCESSOR_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;given_name&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NO_MAPPING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note&lt;/strong&gt;: In the current configuration, &lt;code&gt;groups&lt;/code&gt; in the Access Token Mapping reads from &lt;code&gt;given_name&lt;/code&gt; in the processor policy contract. Verify this is intentional if groups are required in the issued token.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OGNL Expression - Actor Claim Transformation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Expression
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;jsonObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;simple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JSONObject&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;jsonObj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sub"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tepp.actor"&lt;/span&gt;&lt;span class="o"&gt;)),&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;jsonObj&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step-by-Step Breakdown
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new org.json.simple.JSONObject()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create empty JSON object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#this.get("tepp.actor")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read &lt;code&gt;actor&lt;/code&gt; from processor policy output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#jsonObj.put("sub", ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set the &lt;code&gt;sub&lt;/code&gt; field of the JSON object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#jsonObj&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return the constructed object as the claim value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Input → Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;tepp.actor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contact-hr-client"&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="err"&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;"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;"contact-hr-client"&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="err"&gt;▼&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Issued&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JWT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;contains:&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actor"&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;"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;"contact-hr-client"&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;h3&gt;
  
  
  Why a JSON Object?
&lt;/h3&gt;

&lt;p&gt;RFC 8693 §4.1 defines the &lt;code&gt;act&lt;/code&gt; (actor) claim as a &lt;strong&gt;JSON object&lt;/strong&gt;, not a string:&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="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"actor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contact-hr-client"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"actor"&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;"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;"contact-hr-client"&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;This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chain of delegation&lt;/strong&gt;: nested &lt;code&gt;act&lt;/code&gt; objects for multi-hop scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional actor identity&lt;/strong&gt;: can include &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards compliance&lt;/strong&gt;: downstream services can parse it uniformly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AccessTokenManagement Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AccessTokenManagement&lt;/code&gt; is the JWT Access Token Manager that &lt;strong&gt;signs and issues the final token&lt;/strong&gt; after all processor policy and attribute mapping work is complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AccessTokenManagement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AccessTokenManagement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JwtBearerAccessTokenManagementPlugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Token Lifetime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;120 seconds&lt;/td&gt;
&lt;td&gt;~2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Centralized Signing Key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses PF global signing key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWS Algorithm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RS256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSA + SHA-256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Include Key ID (&lt;code&gt;kid&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables key rotation discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Include X.509 Thumbprint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWKS Cache Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;720 minutes&lt;/td&gt;
&lt;td&gt;12 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable Token Revocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No revocation endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWT ID Length&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22 characters&lt;/td&gt;
&lt;td&gt;Unique per token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Include Issued At&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iat&lt;/code&gt; always present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Issuer Claim Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client ID Claim Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope Claim Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Space Delimit Scope Values&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization Details Claim&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;authorization_details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Signing Key
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key Pair ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5jqt7j8mxbwl2awtpc465yzx1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm&lt;/td&gt;
&lt;td&gt;RSA 2048-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature Algorithm&lt;/td&gt;
&lt;td&gt;RS256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage&lt;/td&gt;
&lt;td&gt;Token signing (all JWT tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public JWKS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://id.example.com/pf/JWKS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Attribute Contract
&lt;/h3&gt;

&lt;p&gt;Claims the manager can include in issued tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Multi-Valued&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Custom - credential vault reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;RFC 8693 delegation claim (JSON object)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Subject identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Audience (requesting client)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Granted scopes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Multi-valued - user's group memberships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;User's first name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;family_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;User's surname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;User's email address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Always-Added Standard Claims
&lt;/h3&gt;

&lt;p&gt;Regardless of attribute mapping, these claims are always added automatically:&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://id.example.com"&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;1776667961&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;1776675161&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;"XWcCCzPtj0OFR6HU8UA7Cw"&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;h3&gt;
  
  
  Default Access Token Manager
&lt;/h3&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;"defaultAccessTokenManagerRef"&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;"AccessTokenManagement"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also the global default - used for all standard OAuth flows in addition to token exchange.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Example - HR Chatbot Token Exchange
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;darkedges-hr-chatbot&lt;/code&gt; application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Obtains a &lt;strong&gt;client credentials token&lt;/strong&gt; for itself (&lt;code&gt;actor_token&lt;/code&gt;) at startup via &lt;code&gt;initialize_agent_token()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Receives a &lt;strong&gt;user access token&lt;/strong&gt; after OAuth callback (&lt;code&gt;subject_token&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Performs a token exchange to obtain a &lt;strong&gt;delegated token&lt;/strong&gt; that records both identities&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Token Exchange Request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://id.example.com/as/token.oauth2'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'grant_type=urn:ietf:params:oauth:grant-type:token-exchange'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'subject_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IlA1X1FfaDdqaGVpRkpWQnBVRlh6M2RPRmRGb19SUzI1NiIsInBpLmF0bSI6IjRld3AifQ.eyJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiXSwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbXSwiY2xpZW50X2lkIjoiY29udGFjdC1oci1jbGllbnQiLCJpc3MiOiJodHRwczovL2lkLnBpbmcuZGFya2VkZ2VzLmNvbSIsImlhdCI6MTc3NjY2Nzk1OCwianRpIjoieEV4YWRZRXR1cjhPbEtmOU5XM3BWSSIsInZhdWx0bG9jYXRpb24iOiJyZWZlcmVuY2VpZC9tc2VudHJhaWQvMEJVVGZPS2ZLYkNpMlJmNFMta3JOY0ZRVUFKMlI0WURJcWY4WHZsNW5LNCIsImF1ZCI6ImNvbnRhY3QtaHItY2xpZW50Iiwic3ViIjoidXNlci4xIiwiZ3JvdXBzIjpbIkFkbWluaXN0cmF0b3JzIl0sImZhbWlseV9uYW1lIjoiU2Vhd2VsbCIsImVtYWlsIjoibmlydmluZ3VrQGhvdG1haWwuY29tIiwiZXhwIjoxNzc2Njc1MTU4fQ.Cwk_hCqTocEZoE0yYOFnTjMd6UYBE5BToVOpj51GvNQcHAS76sw0p7pygqm5ze9kxntgyG6OQ8KjKxMUwRmCfC4wZimVRW32-1wTt7UNgKxZcCEAw23VO9XNVgCGdQBShWcqpla8-4cSxU0VIqZJQroVsP9L_hy8mUrRmN7dLWAt2f4KkgNuZmWK7xPbhRUQeIkOcjHhc9FQN4MB08O_DU0on6RbeW54pD0ndsviwMAV3MLLh898DkVSzy2_PpPNr8jgRWPBgcjmAuH2h5a_mcjr6Ei6c0tGOZchS05BwA2qjvWI8w9_C-7Ucn3_GIycIbPCh2ni9dAM9e_CjNfpdg'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'subject_token_type=urn:ietf:params:oauth:token-type:access_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'actor_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IlA1X1FfaDdqaGVpRkpWQnBVRlh6M2RPRmRGb19SUzI1NiIsInBpLmF0bSI6IjRld3AifQ.eyJzY29wZSI6IiIsImF1dGhvcml6YXRpb25fZGV0YWlscyI6W10sImNsaWVudF9pZCI6ImNvbnRhY3QtaHItY2xpZW50IiwiaXNzIjoiaHR0cHM6Ly9pZC5waW5nLmRhcmtlZGdlcy5jb20iLCJpYXQiOjE3NzY2Njc5MzcsImp0aSI6InVqUGVQeHJBVU9MbHJqMDNPUm1QZTEiLCJleHAiOjE3NzY2NzUxMzd9.OEXjyYbBb4KmVMlBZJ8ucnn5_CacufyKL3-E_XsBcQWMhxhm_W9eCOpG3y_xmFGy9wSSNGpPzgVBzeHZ5xyYlSgt2fpBcA2UolQLNT0MKJrbqpJZicqmUh5HalGv6rXG4iuRjpFJ3_-N8zLUrk1t8puZYsSTPaYCrSb1K37_3moPzaNxIgrFplXftax5ez9kgu0QqtA3WyYNJUHAHdFv8cyBbOUy7MMdzTdMlZFaOoO7JHEdFpCzzTkuhtC1D95AADTApvJGsy6Lo4llnJoofnJmmXEjWaAY3hEm2kbXW1he2nR1fZtYQa-_-LxfwR6X5BAxrn96G8JWpc5Y2KKKFw'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'actor_token_type=urn:ietf:params:oauth:token-type:access_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'requested_token_type=urn:ietf:params:oauth:token-type:access_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'client_id=contact-oauth-client'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'client_secret=xxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'scope=openid profile email'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Actor Token Claims (Chatbot - Client Credentials)
&lt;/h3&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;"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;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_details"&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;"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;"contact-hr-client"&lt;/span&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://id.example.com"&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;1776667937&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;"ujPePxrAUOLlrj03ORmPe1"&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;1776675137&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;blockquote&gt;
&lt;p&gt;Actor token has &lt;strong&gt;empty scope&lt;/strong&gt; - obtained via Client Credentials, representing the application identity, not a user.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Subject Token Claims (User - Authorization Code)
&lt;/h3&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;"scope"&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="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_details"&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;"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;"contact-hr-client"&lt;/span&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://id.example.com"&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;1776667958&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;"xExadYEtur8OlKf9NW3pVI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vaultlocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"referenceid/msentraid/0BUTfOKfKbCi2Rf4S-krNcFQUAJ2R4YDIqf8Xvl5nK4"&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;"contact-hr-client"&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.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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="s2"&gt;"Administrators"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Seawell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxxx@hotmail.com"&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;1776675158&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;blockquote&gt;
&lt;p&gt;Subject token has &lt;strong&gt;full user scopes and claims&lt;/strong&gt; - obtained via Authorization Code flow, representing the user's identity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Exchanged Token Claims (Issued by PingFederate)
&lt;/h3&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;"scope"&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="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_details"&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;"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;"contact-oauth-client"&lt;/span&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://id.example.com"&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;1776667961&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;"XWcCCzPtj0OFR6HU8UA7Cw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actor"&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;"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;"contact-hr-client"&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;"vaultlocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"referenceid/msentraid/0BUTfOKfKbCi2Rf4S-krNcFQUAJ2R4YDIqf8Xvl5nK4"&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;"contact-oauth-client"&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.1"&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;1776675161&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;h3&gt;
  
  
  What Changed Between Input and Output
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Subject Token&lt;/th&gt;
&lt;th&gt;Actor Token&lt;/th&gt;
&lt;th&gt;Exchanged Token&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved from subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-hr-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-hr-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-oauth-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requesting client replaces original&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-hr-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contact-oauth-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Audience = requesting client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "sub": "contact-hr-client" }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Added&lt;/strong&gt; - records delegation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["openid","profile","email"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["openid","profile","email"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved from subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaultlocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;preserved&lt;/td&gt;
&lt;td&gt;Passed through&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776667958&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776667937&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776667961&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New issuance time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776675158&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776675137&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1776675161&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New: iat + 120s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jti&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;Fresh JWT ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Chatbot Log Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;DEBUG: Initiating token exchange - actor: eyJhbGciOi..., subject: eyJhbGciOi...
✓ Token exchange successful for user
2026-04-20 16:52:41 - ✓ Token exchange completed for xxxx@hotmail.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Token Lifecycle Sequence
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T0   User authenticates via PingFederate (Authorization Code flow)
     ├─ client_id: contact-hr-client
     ├─ Response: subject_token (user's access token)
     ├─ Lifetime: 120 seconds
     └─ Contains: sub, scope, vaultlocation, groups, email, names

T1   HR Chatbot app starts - initialize_agent_token() called
     ├─ POST /as/token.oauth2
     ├─ grant_type: client_credentials
     ├─ client_id: contact-hr-client
     ├─ Response: actor_token (chatbot's application token)
     ├─ Lifetime: 120 seconds
     └─ Cached globally in app memory + Redis

T2   OAuth callback fires - user token received in session
     ├─ actor_token available (from T1)
     └─ subject_token available (from OAuth callback)

T3   Token exchange request sent to PingFederate
     ├─ /as/token.oauth2
     ├─ grant_type: urn:ietf:params:oauth:grant-type:token-exchange
     ├─ subject_token: user's token (from T0)
     ├─ subject_token_type: urn:ietf:params:oauth:token-type:access_token
     ├─ actor_token: chatbot's token (from T1)
     ├─ actor_token_type: urn:ietf:params:oauth:token-type:access_token
     ├─ client_id: contact-oauth-client
     └─ scope: openid profile email

T4   PingFederate - policy selection
     ├─ PROCESSORPOLICIES selected (default policy)
     └─ Mapping 1 selected (both token types = access_token)

T5   PingFederate - token validation
     ├─ PFSubjectProcessor validates subject_token
     │   ├─ Verify RS256 signature against pf/JWKS
     │   ├─ Check issuer = https://id.example.com ✓
     │   ├─ Check audience = contact-hr-client ✓
     │   ├─ Check exp not reached ✓
     │   └─ Extract: sub, scope, groups, vaultlocation, email, names
     └─ PFActorSubject validates actor_token
         ├─ Verify RS256 signature against pf/JWKS
         ├─ Check issuer = https://id.example.com ✓
         ├─ Check exp not reached ✓
         └─ Extract: client_id

T6   PingFederate - attribute fulfillment
     ├─ actor        ← actor_token.client_id    = "contact-hr-client"
     ├─ subject      ← subject_token.sub        = "user.1"
     ├─ scope        ← subject_token.scope      = ["openid","profile","email"]
     ├─ vaultlocation← subject_token.vaultlocation
     ├─ groups       ← subject_token.groups     = ["Administrators"]
     ├─ given_name   ← subject_token.given_name
     ├─ family_name  ← subject_token.family_name = "Seawell"
     └─ email        ← subject_token.email      = "xxxx@hotmail.com"

T7   Access Token Mapping - OGNL transformation
     └─ actor → { "sub": "contact-hr-client" }  (JSON object per RFC 8693)

T8   AccessTokenManagement - JWT issuance
     ├─ Sign with RS256, key 5jqt7j8mxbwl2awtpc465yzx1
     ├─ Add: iss, iat, exp (iat+120), jti
     └─ Issued token returned

T9   Chatbot receives exchanged token
     └─ Stored in session metadata as "access_token"

T9+120s  Exchanged token expires
          └─ Next user action triggers new token exchange
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Configuration File Locations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Resource Type in data.json&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token Exchange Policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/tokenExchange/processor/policies&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default Policy Setting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/tokenExchange/processor/settings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All Token Processors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/idp/tokenProcessors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access Token Managers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/accessTokenManagers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default ATM Setting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/accessTokenManagers/settings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access Token Mappings&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/accessTokenMappings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing Key Pair&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/keyPairs/signing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OIDC Policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/oauth/openIdConnect/policies&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Source file&lt;/strong&gt;: &lt;a href="//../profiles/pingfederate/bulk-export/shared/data.json"&gt;profiles/pingfederate/bulk-export/shared/data.json&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Validation Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pre-Exchange
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PingFederate Configuration:
☐ PROCESSORPOLICIES exists and is set as default processor policy
☐ Both processor mappings configured (Mapping 1 + Mapping 2)
☐ actorTokenRequired = true
☐ AccessTokenManagement token manager exists
☐ Signing key 5jqt7j8mxbwl2awtpc465yzx1 is valid and not expired
☐ JWKS endpoint accessible: https://id.example.com/pf/JWKS

PFSubjectProcessor:
☐ Issuer: https://id.example.com
☐ Audience check enabled, value: contact-hr-client
☐ Expiration + Issued At checks enabled
☐ JWKS URL reachable

PFActorSubject:
☐ Issuer: https://id.example.com
☐ Audience check disabled
☐ Expiration + Issued At checks enabled
☐ client_id in attribute contract

MSFTTOKENPROCESSOR:
☐ Both Azure issuers configured
☐ Both JWKS URLs reachable
☐ Required audience values present

PFTOKENPROCESSOR:
☐ References AccessTokenManagement
☐ Scope handling configured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Per-Request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subject Token:
☐ JWT (3 dot-separated parts)
☐ RS256 signature valid
☐ iss = https://id.example.com
☐ aud = contact-hr-client
☐ exp &amp;gt; now (not expired)
☐ iat present and reasonable
☐ sub claim present
☐ scope claim present

Actor Token:
☐ JWT (3 dot-separated parts)
☐ RS256 signature valid
☐ iss = https://id.example.com
☐ exp &amp;gt; now (not expired)
☐ client_id claim present

Request Parameters:
☐ grant_type = urn:ietf:params:oauth:grant-type:token-exchange
☐ subject_token_type = urn:ietf:params:oauth:token-type:access_token
☐ actor_token_type = urn:ietf:params:oauth:token-type:access_token
☐ actor_token present (required by policy)
☐ client_id = contact-oauth-client (or other authorised client)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issued Token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JWT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;signed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RS&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;jqt&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;j&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;mxbwl&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;awtpc&lt;/span&gt;&lt;span class="mi"&gt;465&lt;/span&gt;&lt;span class="err"&gt;yzx&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;iss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://id.example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preserved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subject_token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;actor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;present&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object:&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;"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;"&amp;lt;client_id&amp;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="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aud&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;requesting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;client_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;iat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jti&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unique&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="err"&gt;-character&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vaultlocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preserved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;present&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;☐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;matches&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;requested&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scopes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Agent access token not available, skipping token exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initialize_agent_token()&lt;/code&gt; failed at startup&lt;/td&gt;
&lt;td&gt;Check Redis connectivity; verify PF &lt;code&gt;/as/token.oauth2&lt;/code&gt; is reachable; check &lt;code&gt;contact-hr-client&lt;/code&gt; credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;invalid_request&lt;/code&gt; on token exchange&lt;/td&gt;
&lt;td&gt;Missing required parameter or wrong token type&lt;/td&gt;
&lt;td&gt;Confirm &lt;code&gt;actor_token&lt;/code&gt; and &lt;code&gt;actor_token_type&lt;/code&gt; present; verify token type URNs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;invalid_token&lt;/code&gt; on subject or actor&lt;/td&gt;
&lt;td&gt;JWT validation failed&lt;/td&gt;
&lt;td&gt;Check token not expired; verify issuer; confirm audience matches processor config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;actor&lt;/code&gt; claim missing from issued token&lt;/td&gt;
&lt;td&gt;OGNL expression failed&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;org.json.simple&lt;/code&gt; library available; verify &lt;code&gt;tepp.actor&lt;/code&gt; is populated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;actor&lt;/code&gt; claim is string not object&lt;/td&gt;
&lt;td&gt;Wrong mapping - not using OGNL&lt;/td&gt;
&lt;td&gt;Ensure Access Token Mapping uses EXPRESSION source for &lt;code&gt;actor&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;vaultlocation&lt;/code&gt; missing from issued token&lt;/td&gt;
&lt;td&gt;Attribute fulfillment issue&lt;/td&gt;
&lt;td&gt;Confirm subject token contains &lt;code&gt;vaultlocation&lt;/code&gt;; check Mapping 1 contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token exchange succeeds but groups empty&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;groups&lt;/code&gt; maps from &lt;code&gt;given_name&lt;/code&gt; in current config&lt;/td&gt;
&lt;td&gt;Review Access Token Mapping - &lt;code&gt;groups&lt;/code&gt; attribute source currently points to &lt;code&gt;given_name&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout acquiring actor token&lt;/td&gt;
&lt;td&gt;PingFederate slow or unreachable&lt;/td&gt;
&lt;td&gt;10-second timeout in chatbot; check network/TLS; check PF health&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RFC 8693&lt;/strong&gt; - &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693" rel="noopener noreferrer"&gt;OAuth 2.0 Token Exchange&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;§1.1: Delegation vs. Impersonation Semantics&lt;/li&gt;
&lt;li&gt;§4.1: &lt;code&gt;act&lt;/code&gt; (Actor) Claim&lt;/li&gt;
&lt;li&gt;§4.2: &lt;code&gt;scope&lt;/code&gt; Claim&lt;/li&gt;
&lt;li&gt;§4.3: &lt;code&gt;client_id&lt;/code&gt; Claim&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;PingFederate 12.3 Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.pingidentity.com/pingfederate/12.3/administrators_reference_guide/pf_config_oauth_token_exchange.html" rel="noopener noreferrer"&gt;Token Exchange Processor Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pingidentity.com/pingfederate/12.3/administrators_reference_guide/pf_access_token_managers.html" rel="noopener noreferrer"&gt;Access Token Managers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pingidentity.com/pingfederate/12.3/administrators_reference_guide/pf_token_processors.html" rel="noopener noreferrer"&gt;Token Processors&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Project Files&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//../TOKENEXCHANGE.md"&gt;TOKENEXCHANGE.md&lt;/a&gt; - Token exchange curl examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="//../profiles/pingfederate/bulk-export/shared/data.json"&gt;data.json&lt;/a&gt; - Full PingFederate configuration export&lt;/li&gt;
&lt;li&gt;
&lt;a href="//../../chatbot/darkedges-hr-chatbot/app.py"&gt;darkedges-hr-chatbot/app.py&lt;/a&gt; - Chatbot token exchange implementation&lt;/li&gt;
&lt;li&gt;
&lt;a href="//../../chatbot/darkedges-hr-chatbot/auth_handler.py"&gt;darkedges-hr-chatbot/auth_handler.py&lt;/a&gt; - &lt;code&gt;perform_token_exchange()&lt;/code&gt; method&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>backend</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>Weekend Build Recap: Trust-Aware API Access with OpenID Federation</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sun, 12 Apr 2026 20:51:16 +0000</pubDate>
      <link>https://dev.to/darkedges/weekend-build-recap-trust-aware-api-access-with-openid-federation-3o3j</link>
      <guid>https://dev.to/darkedges/weekend-build-recap-trust-aware-api-access-with-openid-federation-3o3j</guid>
      <description>&lt;p&gt;This weekend we built and validated a trust-driven access control flow across our OpenID Federation demo stack.&lt;/p&gt;

&lt;p&gt;Core outcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;app.idamaas.xyz&lt;/code&gt; is not an active trusted subordinate, API access is blocked.&lt;/li&gt;
&lt;li&gt;If the required trust mark for &lt;code&gt;oidfapi.verifymy.id&lt;/code&gt; is revoked or missing, API access is blocked.&lt;/li&gt;
&lt;li&gt;When either trust condition is restored, access is restored.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We Developed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Federation-aware app validation in &lt;code&gt;app.idamaas.xyz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Trust-anchor-backed trust mark enforcement (no self-asserted shortcut).&lt;/li&gt;
&lt;li&gt;Admin trust mark lifecycle controls: issue, revoke, enable, cleanup.&lt;/li&gt;
&lt;li&gt;Diagnostics endpoints to explain trust decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo Story 1: &lt;code&gt;app.idamaas.xyz&lt;/code&gt; Enabled/Disabled
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enabled (expected success)
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;app.idamaas.xyz&lt;/code&gt; is active as a subordinate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/demo/discover-and-call returns ok: true&lt;/li&gt;
&lt;li&gt;apiResult.ok is true&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvf1irgr1sxg9ck76zo1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvf1irgr1sxg9ck76zo1.png" alt="app.idamaas.xyz enabled in admin UI" width="800" height="1673"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkozs4ahwbbaspbsbmtu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkozs4ahwbbaspbsbmtu7.png" alt="discover-and-call success response" width="800" height="1035"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Disabled (expected block)
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;app.idamaas.xyz&lt;/code&gt; is deactivated/deleted as subordinate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/demo/discover-and-call returns HTTP 403&lt;/li&gt;
&lt;li&gt;error is client_not_trusted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz8xdzwycy769ra3031i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz8xdzwycy769ra3031i.png" alt="app.idamaas.xyz disabled/deleted in admin UI" width="800" height="1491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vdnjh4167q2ror6uk7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vdnjh4167q2ror6uk7i.png" alt=" discover-and-call success response" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Story 2: Trust Mark Enabled/Disabled
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Trust mark enabled (expected success)
&lt;/h3&gt;

&lt;p&gt;Required trust mark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;issuer: &lt;code&gt;https://trust-anchor.zkp.au&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;id: urn:darkedges:trustmark:identity-verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When active:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trustMarkValidation.ok is true&lt;/li&gt;
&lt;li&gt;/demo/discover-and-call succeeds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g9dr2hisp3s54670xjb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g9dr2hisp3s54670xjb.png" alt="trust mark enabled/restored in admin UI" width="800" height="1112"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkozs4ahwbbaspbsbmtu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkozs4ahwbbaspbsbmtu7.png" alt="discover-and-call success response" width="800" height="1035"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust mark revoked (expected block)
&lt;/h3&gt;

&lt;p&gt;When required trust mark is revoked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/demo/discover-and-call returns HTTP 403&lt;/li&gt;
&lt;li&gt;error is required_trust_mark_missing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwbz9az0udwrw8otgmr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwbz9az0udwrw8otgmr8.png" alt="trust mark revoked in admin UI" width="800" height="1112"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1cat0vxqdvqm0k36h5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1cat0vxqdvqm0k36h5m.png" alt=" discover-and-call success response" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Endpoints Used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;app demo flow: &lt;code&gt;GET /demo/discover-and-call&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;app diagnostics: &lt;code&gt;GET /demo/federation-details&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;trust mark list: &lt;code&gt;GET /federation_trust_mark_list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;trust mark fetch: &lt;code&gt;GET /federation_trust_mark&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;subordinate admin: &lt;code&gt;/admin/subordinates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;trust mark admin: &lt;code&gt;/admin/trust-marks&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational Outcome
&lt;/h2&gt;

&lt;p&gt;By the end of the weekend, trust state directly controlled access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;disable subordinate&lt;/code&gt; -&amp;gt; &lt;code&gt;blocked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;revoke trust mark&lt;/code&gt; -&amp;gt; &lt;code&gt;blocked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;re-enable either control&lt;/code&gt; -&amp;gt; &lt;code&gt;restored&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us a practical, explainable trust control model for federation demos and production hardening.&lt;/p&gt;

&lt;h1&gt;
  
  
  OpenIDFederation #TrustAnchor #TrustMarks #ZeroTrust
&lt;/h1&gt;

</description>
      <category>api</category>
      <category>devjournal</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Enriching Vault OIDC Tokens with SPIFFE Identity Metadata using Terraform</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Wed, 03 Dec 2025 19:39:08 +0000</pubDate>
      <link>https://dev.to/darkedges/enriching-vault-oidc-tokens-with-spiffe-identity-metadata-using-terraform-314g</link>
      <guid>https://dev.to/darkedges/enriching-vault-oidc-tokens-with-spiffe-identity-metadata-using-terraform-314g</guid>
      <description>&lt;p&gt;In modern microservices architectures, machine identity is just as critical as human identity. When services communicate, they often need to prove not just &lt;em&gt;who&lt;/em&gt; they are, but &lt;em&gt;what&lt;/em&gt; they are—their environment, business unit, or SPIFFE ID.&lt;/p&gt;

&lt;p&gt;HashiCorp Vault is excellent for this. Its Identity Secrets Engine can issue OIDC tokens that serve as verifiable credentials for your workloads. However, getting those tokens to contain rich, custom metadata requires connecting a few specific dots: &lt;strong&gt;AppRole&lt;/strong&gt;, &lt;strong&gt;Identity Entities&lt;/strong&gt;, and &lt;strong&gt;OIDC Templates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through how to implement a robust machine identity pipeline using Terraform, where an application authenticates via AppRole and receives an OIDC token enriched with custom metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;We want an application (e.g., a "ChatBot") to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate to Vault using the &lt;strong&gt;AppRole&lt;/strong&gt; method.&lt;/li&gt;
&lt;li&gt;Request an &lt;strong&gt;OIDC token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Receive a token containing custom claims like &lt;code&gt;spiffe_id&lt;/code&gt;, &lt;code&gt;business_unit&lt;/code&gt;, and &lt;code&gt;environment&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Define the Identity Entity
&lt;/h2&gt;

&lt;p&gt;First, we define the "who". In Vault, an &lt;strong&gt;Entity&lt;/strong&gt; represents a unique identity. This is where we store the metadata we want to eventually see in our token.&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="c1"&gt;# identities.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"vault_identity_entity"&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_identities_map&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;

  &lt;span class="c1"&gt;# This metadata is what we'll inject into the token later&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
    &lt;span class="nx"&gt;business_unit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;business_unit&lt;/span&gt;
    &lt;span class="nx"&gt;spiffe_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spiffe://vault/application/${each.value.identity.environment}/${each.value.identity.business_unit}/${each.value.identity.name}"&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;h2&gt;
  
  
  Step 2: Configure AppRole Authentication
&lt;/h2&gt;

&lt;p&gt;Next, we configure the &lt;strong&gt;AppRole&lt;/strong&gt; auth backend. This is the standard way for machines to authenticate.&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="c1"&gt;# approle.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"vault_approle_auth_backend_role"&lt;/span&gt; &lt;span class="s2"&gt;"applications"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_identities_map&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_auth_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;
  &lt;span class="nx"&gt;role_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="nx"&gt;token_ttl&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
  &lt;span class="nx"&gt;bind_secret_id&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;h3&gt;
  
  
  The "Secret Sauce": Binding AppRole to the Entity
&lt;/h3&gt;

&lt;p&gt;This is the most critical step. By default, when you log in with AppRole, Vault creates a generic entity for that role. To use our custom metadata defined in Step 1, we must explicitly bind the AppRole to our specific Entity using an &lt;strong&gt;Entity Alias&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Gotcha:&lt;/strong&gt; When creating an alias for AppRole, the &lt;code&gt;name&lt;/code&gt; of the alias must be the &lt;strong&gt;Role ID&lt;/strong&gt;, not the Role Name.&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="c1"&gt;# approle.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"vault_identity_entity_alias"&lt;/span&gt; &lt;span class="s2"&gt;"approle_applications"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_identities_map&lt;/span&gt;

  &lt;span class="c1"&gt;# CRITICAL: Use role_id, not role_name&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_approle_auth_backend_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applications&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;role_id&lt;/span&gt;

  &lt;span class="nx"&gt;mount_accessor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_auth_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessor&lt;/span&gt;
  &lt;span class="nx"&gt;canonical_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_identity_entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to ensure the AppRole inherits the entity's properties. We can enforce this via a generic endpoint configuration:&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;"vault_generic_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"approle_entity_inherit"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_identities_map&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vault_approle_auth_backend_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applications&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;"auth/approle/role/${each.key}"&lt;/span&gt;

  &lt;span class="nx"&gt;data_json&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;entity_alias_sole_inherit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Configure the OIDC Template
&lt;/h2&gt;

&lt;p&gt;Finally, we configure the OIDC provider and role. The &lt;code&gt;template&lt;/code&gt; field is where the magic happens. We use Vault's template syntax to pull values dynamically from the authenticated entity's metadata.&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="c1"&gt;# identity_tokens.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"vault_identity_oidc_role"&lt;/span&gt; &lt;span class="s2"&gt;"application_identity"&lt;/span&gt; &lt;span class="p"&gt;{&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;"application_identity"&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_identity_oidc_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;client_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spiffe://vault.darkedges.au/gateway"&lt;/span&gt;
  &lt;span class="nx"&gt;ttl&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;

  &lt;span class="c1"&gt;# The Template: Injecting metadata into the JSON payload&lt;/span&gt;
  &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
{
  "azp": {{identity.entity.metadata.spiffe_id}},
  "nbf": {{time.now}},
  "groups": {{identity.entity.groups.names}},
  "appinfo": {
    "business_unit": {{identity.entity.metadata.business_unit}},
    "environment": {{identity.entity.metadata.environment}}
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOT
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Testing the Flow
&lt;/h2&gt;

&lt;p&gt;Now, let's see it in action. We'll use PowerShell to simulate the application workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Get Credentials &amp;amp; Login
&lt;/h3&gt;

&lt;p&gt;First, we retrieve the Role ID and Secret ID, then authenticate to get a Vault token.&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="c"&gt;# Get Role ID and Secret ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker-compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auth/approle/role/ChatBot/role-id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SECRET_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker-compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auth/approle/role/ChatBot/secret-id&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$APPTOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker-compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auth/approle/login&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;secret_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECRET_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Request the OIDC Token
&lt;/h3&gt;

&lt;p&gt;Using the Vault token we just got, we request the OIDC token.&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="nv"&gt;$OIDC_TOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker-compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APPTOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;identity/oidc/token/application_identity&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Result
&lt;/h3&gt;

&lt;p&gt;When we decode the JWT, we see our rich metadata is successfully populated!&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;"appinfo"&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;"business_unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"engineering"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&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;"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;"spiffe://vault.darkedges.au/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;"azp"&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://vault/application/production/engineering/ChatBot"&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;1764848993&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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;"ChatBot group"&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;1764762593&lt;/span&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://vault.darkedges.au/v1/identity/oidc"&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;"ea0e0006-d4f5-cbde-3cb6-c013d4dba5f2"&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By combining &lt;strong&gt;Identity Entities&lt;/strong&gt;, &lt;strong&gt;AppRole Aliases&lt;/strong&gt;, and &lt;strong&gt;OIDC Templates&lt;/strong&gt;, we've turned Vault into a powerful identity provider for our internal services. This setup allows downstream systems (like API gateways or service meshes) to make authorization decisions based on trusted, verifiable metadata like &lt;code&gt;business_unit&lt;/code&gt; or &lt;code&gt;environment&lt;/code&gt;, rather than just a raw IP address or generic token.&lt;/p&gt;

&lt;p&gt;a complete example is available at &lt;a href="https://github.com/darkedges/spiffe-vault-terraform" rel="noopener noreferrer"&gt;https://github.com/darkedges/spiffe-vault-terraform&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ausmartway/vault-config-as-code" rel="noopener noreferrer"&gt;https://github.com/ausmartway/vault-config-as-code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/macquarie-engineering-blog/embracing-modern-identity-with-spiffe-and-hashicorp-vault-a-macquarie-bank-journey-9f17ae748f82" rel="noopener noreferrer"&gt;https://medium.com/macquarie-engineering-blog/embracing-modern-identity-with-spiffe-and-hashicorp-vault-a-macquarie-bank-journey-9f17ae748f82&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Auto-Detecting CSV Schemas for Lightning-Fast ClickHouse Ingestion with Parquet</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Fri, 07 Nov 2025 09:06:41 +0000</pubDate>
      <link>https://dev.to/darkedges/auto-detecting-csv-schemas-for-lightning-fast-clickhouse-ingestion-with-parquet-34d9</link>
      <guid>https://dev.to/darkedges/auto-detecting-csv-schemas-for-lightning-fast-clickhouse-ingestion-with-parquet-34d9</guid>
      <description>&lt;p&gt;As a data engineer, one of the most repetitive tasks I face is ingesting data from CSV files. The problem isn't just loading the data; it's the ceremony that comes with it. Every time a new data source appears, I have to manually inspect the columns, define a table schema, and write a script to load it. What if the CSV has 100 columns? What if the data types are ambiguous? This process is tedious and error-prone.&lt;/p&gt;

&lt;p&gt;I wanted a better way. My goal was to create a Node.js script that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Read any CSV file&lt;/strong&gt; without prior knowledge of its structure.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Auto-detect the schema&lt;/strong&gt;, including column names and data types.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Convert the CSV to Parquet&lt;/strong&gt;, a highly efficient columnar storage format.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Prepare for ingestion&lt;/strong&gt; into ClickHouse, which loves Parquet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this article, I'll walk you through the proof-of-concept I built to solve this exact problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Parquet and ClickHouse?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ClickHouse&lt;/strong&gt; is an open-source, column-oriented database built for speed. It's a beast for analytical queries (OLAP).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Parquet&lt;/strong&gt; is a columnar storage format. Instead of storing data in rows, it stores it in columns. This is a perfect match for ClickHouse because it allows the database to read only the columns it needs for a query, dramatically reducing I/O and speeding up performance.&lt;/p&gt;

&lt;p&gt;Combining the two means you get efficient storage and lightning-fast analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concept: From CSV to Parquet
&lt;/h2&gt;

&lt;p&gt;Our script will perform a three-step process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Schema Detection&lt;/strong&gt;: Read the CSV headers and a few sample rows to infer the data type of each column (e.g., &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Data Transformation&lt;/strong&gt;: Convert the raw string values from the CSV into their inferred types.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Parquet Conversion&lt;/strong&gt;: Write the transformed data and the detected schema into a new &lt;code&gt;.parquet&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start with a simple CSV file to demonstrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our Example CSV: &lt;code&gt;sample-data.csv&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,first_name,last_name,email,is_active,created_at,balance
1,John,Doe,john.doe@example.com,true,2023-01-15T10:00:00Z,150.75
2,Jane,Smith,jane.smith@example.com,false,2023-02-20T11:30:00Z,200.00
3,Peter,Jones,peter.jones@example.com,true,2023-03-10T09:05:00Z,50.25
4,Mary,Williams,mary.w@example.com,true,2023-04-01T15:45:00Z,300.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file has a nice mix of data types: integers, strings, booleans, timestamps, and floating-point numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Setting Up the Project
&lt;/h2&gt;

&lt;p&gt;First, let's set up a simple Node.js project and install the necessary libraries. We'll use &lt;code&gt;papaparse&lt;/code&gt; for robust CSV parsing and &lt;code&gt;parquetjs&lt;/code&gt; for writing Parquet files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;papaparse parquetjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Auto-Detecting the Schema
&lt;/h2&gt;

&lt;p&gt;This is the core of our solution. We need a function that can look at the string data from the CSV and make an educated guess about its real type.&lt;/p&gt;

&lt;p&gt;Here’s a simple type inference function. It checks if a value is a boolean, a number, or a date. If it's none of those, it defaults to a string.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.js&lt;/code&gt;&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="c1"&gt;// Function to infer data type from a string value&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;inferType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BOOLEAN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOUBLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Use DOUBLE for all numbers for simplicity&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UTF8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Dates will be stored as strings (UTF8)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UTF8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Default to string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can use this to build a schema from the CSV file. We'll read the first data row to infer the types for each column.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.js&lt;/code&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;papa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;papaparse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... inferType function from above ...&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;papa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;header&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="na"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error parsing CSV for schema detection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;firstRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;firstRecord&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;inferType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;schema&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;When we run &lt;code&gt;detectSchema('sample-data.csv')&lt;/code&gt;, it will produce an object like this, which is exactly the format &lt;code&gt;parquetjs&lt;/code&gt; needs:&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;"id"&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;"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;"DOUBLE"&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;"first_name"&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;"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;"UTF8"&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;"last_name"&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;"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;"UTF8"&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;"email"&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;"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;"UTF8"&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;"is_active"&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;"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;"BOOLEAN"&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;"created_at"&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;"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;"UTF8"&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;"balance"&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;"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;"DOUBLE"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Converting CSV to Parquet
&lt;/h2&gt;

&lt;p&gt;With the schema detected, we can now read the entire CSV and write it to a Parquet file. The process involves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Creating a &lt;code&gt;ParquetSchema&lt;/code&gt; and a &lt;code&gt;ParquetWriter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Iterating through the CSV rows.&lt;/li&gt;
&lt;li&gt; Casting each value to its detected type (e.g., converting the string &lt;code&gt;"true"&lt;/code&gt; to the boolean &lt;code&gt;true&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Appending the transformed row to the writer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the code to bring it all together:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.js&lt;/code&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ParquetSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParquetWriter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;parquetjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... detectSchema and inferType functions ...&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertCsvToParquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;csvPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parquetPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Detecting schema...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;detectSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;csvPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ParquetSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schemaDefinition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ParquetWriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parquetPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;csvPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;papa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;header&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="na"&gt;skipEmptyLines&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&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;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; records. Converting to Parquet...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processedRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schemaDefinition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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="c1"&gt;// Cast values to their proper types&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BOOLEAN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;processedRow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOUBLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;processedRow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;processedRow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processedRow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Parquet file written to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parquetPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run the conversion&lt;/span&gt;
&lt;span class="nf"&gt;convertCsvToParquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sample-data.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.parquet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running this script (&lt;code&gt;node index.js&lt;/code&gt;), you'll have an &lt;code&gt;output.parquet&lt;/code&gt; file ready for ClickHouse!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Ingesting into ClickHouse
&lt;/h2&gt;

&lt;p&gt;Now for the easy part. Ingesting the Parquet file into ClickHouse is incredibly simple. First, you need a table with a matching schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;my_data&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="n"&gt;Float64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can use the &lt;code&gt;clickhouse-client&lt;/code&gt; to ingest the file directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clickhouse-client &lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO my_data FORMAT Parquet"&lt;/span&gt; &amp;lt; output.parquet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClickHouse reads the Parquet file's schema and streams the data directly into the table. It's fast, efficient, and requires no complex &lt;code&gt;INSERT&lt;/code&gt; statements with tons_of_values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By automating schema detection and converting CSVs to Parquet, we've created a powerful and reusable ETL pipeline. This approach saves a massive amount of time, reduces manual errors, and leverages the high-performance capabilities of both Parquet and ClickHouse.&lt;/p&gt;

&lt;p&gt;This proof-of-concept can be expanded with more robust type detection, error handling, and integration into a larger data workflow, but it's a fantastic starting point for streamlining your data ingestion process.&lt;/p&gt;

</description>
      <category>node</category>
      <category>dataengineering</category>
      <category>database</category>
      <category>automation</category>
    </item>
    <item>
      <title>Uprading Ping Identity Platform to 8.0.0</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Fri, 22 Aug 2025 04:25:24 +0000</pubDate>
      <link>https://dev.to/darkedges/uprading-ping-identity-platform-to-800-5hj2</link>
      <guid>https://dev.to/darkedges/uprading-ping-identity-platform-to-800-5hj2</guid>
      <description>&lt;h2&gt;
  
  
  Ping AM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue with new config when running amupgrade
&lt;/h3&gt;

&lt;p&gt;The following appears to be added in the wrong place with incorrect details&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp4tpzznv2vkzhvpujow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp4tpzznv2vkzhvpujow.png" alt=" " width="354" height="106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error on Startup after migration
&lt;/h3&gt;

&lt;p&gt;When PingAM Starts and you attempt to connect it fails with this in the log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dfq-am  | {"timestamp":"2025-08-22T04:24:20.258Z","level":"WARN","thread":"http-nio-8080-exec-10","mdc":{"transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-38"},"logger":"org.forgerock.openam.core.realms.impl.DefaultRealmLookup","message":"DefaultRealms:lookup Unable to find Org name for: serverinfo","context":"default","transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-38"}
dfq-am  | {"timestamp":"2025-08-22T04:24:20.397Z","level":"WARN","thread":"http-nio-8080-exec-2","mdc":{"transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"},"logger":"org.forgerock.openam.core.realms.impl.DefaultRealmLookup","message":"DefaultRealms:lookup Unable to find Org name for: sessions","context":"default","transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"}
dfq-am  | {"timestamp":"2025-08-22T04:24:20.414Z","level":"WARN","thread":"http-nio-8080-exec-2","mdc":{"transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"},"logger":"com.sun.identity.monitoring.MonitoringServicesImpl","message":"JDMK runtime not found - Policy Monitoring disabled","context":"default","transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"}
dfq-am  | {"timestamp":"2025-08-22T04:24:20.523Z","level":"WARN","thread":"http-nio-8080-exec-2","mdc":{"transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"},"logger":"com.sun.identity.idm.plugins.internal.AgentsRepo","message":"AgentsRepo.getAgentGroupConfig: Unable to get Agent Group Config due to The instance agentgroup does not exist","context":"default","transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-43"}
dfq-am  | {"timestamp":"2025-08-22T04:24:20.637Z","level":"WARN","thread":"http-nio-8080-exec-1","mdc":{"transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-56"},"logger":"org.forgerock.openam.core.rest.authn.http.AuthenticationServiceV2","message":"Authentication encountered an error: [Status: 401 Unauthorized]","context":"default","transactionId":"4a84d7a7-8b56-4819-b5c6-6adc4d215e2e-56"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it is missing the following entry in Ping DS&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dn: ou=agentgroup,ou=Instances,ou=1.0,ou=AgentService,ou=services,ou=am-config
objectClass: top
objectClass: sunServiceComponent
objectClass: organizationalUnit
ou: agentgroup
sunserviceID: agentgroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwju9qqnd87h7uu21wuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwju9qqnd87h7uu21wuk.png" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating a Workflow using your Connector</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 06:34:34 +0000</pubDate>
      <link>https://dev.to/darkedges/creating-a-workflow-using-your-connector-54fk</link>
      <guid>https://dev.to/darkedges/creating-a-workflow-using-your-connector-54fk</guid>
      <description>&lt;p&gt;In order to use connector you need to &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Connection&lt;/li&gt;
&lt;li&gt;Create a Flow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a Connection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect to your Okta Workflow instance.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Connections&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;code&gt;Connections&lt;/code&gt; click the &lt;code&gt;New Connection&lt;/code&gt; icon.&lt;/li&gt;
&lt;li&gt;Select Your connector 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnhwcpcpu44wbsen73e1.png" alt=" " width="545" height="192"&gt;
&lt;/li&gt;
&lt;li&gt;Provide the details and click the &lt;code&gt;Create&lt;/code&gt; button. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzq9m3ydp0qsik3zvyf2c.png" alt=" " width="562" height="870"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect to your Okta Workflow instance.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Flows&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click the &lt;code&gt;New Flow&lt;/code&gt; button.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;When this happens&lt;/code&gt; block click &lt;code&gt;Add event&lt;/code&gt; and select &lt;code&gt;API Endpoint&lt;/code&gt;. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjf5yvxnlobt0t3kbiig.png" alt=" " width="513" height="571"&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;save&lt;/code&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmp4m4iu95doy7gi7kmc.png" alt=" " width="504" height="337"&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;Then do this&lt;/code&gt; block click &lt;code&gt;Add function&lt;/code&gt; to add functions.&lt;/li&gt;
&lt;li&gt;When finished it should look like. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhuzuisi63dysihbxuq1.png" alt=" " width="800" height="273"&gt;
&lt;/li&gt;
&lt;li&gt;Save it as the name you want to display when selecting the action. Ensure you select ``&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Test Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;code&gt;Run&lt;/code&gt; button and click &lt;code&gt;Run&lt;/code&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhhw9sohmwam5leb9lrd.png" alt=" " width="370" height="460"&gt;
&lt;/li&gt;
&lt;li&gt;When it has completed the execution it should be successful. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8hlcbqdfvgp114jvw4k.png" alt=" " width="800" height="239"&gt;
&lt;/li&gt;
&lt;li&gt;Open the flow directly and it should return similair to 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fanp4i309sxo9zfbpo7x5.png" alt=" " width="275" height="20"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
{&lt;br&gt;
    "output": {&lt;br&gt;
        "length": 1&lt;br&gt;
    },&lt;br&gt;
    "statusCode": 200&lt;br&gt;
}&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating a Connector using the Okta Workflow Connector Builder</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 06:33:53 +0000</pubDate>
      <link>https://dev.to/darkedges/creating-a-connector-using-the-okta-workflow-connector-builder-1l90</link>
      <guid>https://dev.to/darkedges/creating-a-connector-using-the-okta-workflow-connector-builder-1l90</guid>
      <description>&lt;p&gt;Okta Workflow is a no code development environment to create workflows to perform a large number of operations based on Connections. If there is not a Connector available you can create your own using their Connector Builder, and the best part is that you can try it out using their free Integration account.&lt;/p&gt;

&lt;p&gt;Okta workflow no code approach is drag and drop between functions / actions. Most functions have &lt;code&gt;Inputs&lt;/code&gt; and &lt;code&gt;Outputs&lt;/code&gt;, so you can drag an &lt;code&gt;Output&lt;/code&gt; onto an &lt;code&gt;Input&lt;/code&gt;. Once done you can highlight &lt;code&gt;Input&lt;/code&gt; or &lt;code&gt;Output&lt;/code&gt; to see the connection.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag8so80vga3287xu1dbs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag8so80vga3287xu1dbs.gif" alt=" " width="1100" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the basic steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Connector.&lt;/li&gt;
&lt;li&gt;Configure Authentication.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;httpHelper&lt;/code&gt; flow - used by the connector to make requests using the connection Acces token.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;_pingAuth&lt;/code&gt; flow - Validates the Access Token to see if it needs to be renewed.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;action&lt;/code&gt; flow- Performs an operation that can be used in Flow.&lt;/li&gt;
&lt;li&gt;Deploy it&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Connector.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect to your Okta Workflow instance.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Connector Builder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;code&gt;Connectors&lt;/code&gt; click the &lt;code&gt;+&lt;/code&gt; icon.&lt;/li&gt;
&lt;li&gt;Provide the name for the connector and click the &lt;code&gt;Save&lt;/code&gt; button.&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvpxywq1ni3uo8dh9zqgo.jpeg" alt=" " width="574" height="480"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configure Authentication.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;code&gt;Add Authentication&lt;/code&gt; button and fill out the details &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjswa5ngdihpekcnwwcm8.png" alt=" " width="799" height="484"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Test Connection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;code&gt;Test Connections&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;+New Connection&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Provide the details and click the &lt;code&gt;Create&lt;/code&gt; button 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphr96iwgcyevgmf71mvg.png" alt=" " width="604" height="867"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a &lt;code&gt;httpHelper&lt;/code&gt; flo.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;code&gt;Flows&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;+New Flow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;When this happens&lt;/code&gt; block click &lt;code&gt;Add event&lt;/code&gt; and select &lt;code&gt;Helper Flow&lt;/code&gt;. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9rydl0p775r4gvzn8eb.png" alt=" " width="800" height="375"&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;Then do this&lt;/code&gt; block click &lt;code&gt;Add function&lt;/code&gt; to add functions.&lt;/li&gt;
&lt;li&gt;When finished it should look like. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5oyxzh94m8vv8aunoux7.png" alt=" " width="800" height="252"&gt;
&lt;/li&gt;
&lt;li&gt;Save it as &lt;code&gt;httpHelper&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a &lt;code&gt;_pingAuth&lt;/code&gt; flo.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;code&gt;Flows&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;+New Flow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;When this happens&lt;/code&gt; block click &lt;code&gt;Add event&lt;/code&gt; and select &lt;code&gt;Authping&lt;/code&gt;. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq3z96648g1en07tdkpn.png" alt=" " width="785" height="337"&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;Then do this&lt;/code&gt; block click &lt;code&gt;Add function&lt;/code&gt; to add functions.&lt;/li&gt;
&lt;li&gt;When finished it should look like. &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjavnhfhw3299k36sqhsb.png" alt=" " width="800" height="400"&gt;
&lt;/li&gt;
&lt;li&gt;Save it as &lt;code&gt;_authPing&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a &lt;code&gt;action&lt;/code&gt; flo.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;code&gt;Flows&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;+New Flow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;When this happens&lt;/code&gt; block click &lt;code&gt;Add event&lt;/code&gt; and select &lt;code&gt;Action&lt;/code&gt;. &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpy95o2n96zfjgbgc1d9.png" alt=" " width="776" height="547"&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;Then do this&lt;/code&gt; block click &lt;code&gt;Add function&lt;/code&gt; to add functions.&lt;/li&gt;
&lt;li&gt;When finished it should look like. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus21tjkz49ezoubqak5u.png" alt=" " width="800" height="561"&gt;
&lt;/li&gt;
&lt;li&gt;Save it as the name you want to display when selecting the action.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;code&gt;Deployment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Validate connector&lt;/code&gt; and then &lt;code&gt;Done&lt;/code&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz5qjkkj81yx93zm3d0s.png" alt=" " width="250" height="85"&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Deploy test version&lt;/code&gt; 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd7h92luyjf80ur5jdmcg.png" alt=" " width="350" height="63"&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Deploy local connector&lt;/code&gt; 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2ig8j3v3tqz2p11h7uq.png" alt=" " width="356" height="61"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gets a connector deployed and ready for using in an actual flow.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Finally testing the solution</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 02:09:24 +0000</pubDate>
      <link>https://dev.to/darkedges/finally-testing-the-solution-3bkb</link>
      <guid>https://dev.to/darkedges/finally-testing-the-solution-3bkb</guid>
      <description>&lt;p&gt;To recap, we have an Private Application Load Balancer, Fargate ECS Cluster and an AWS API Gateway v2. So know we can test the solution. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>apigateway</category>
      <category>systemdesign</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
