<?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.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>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>
    <item>
      <title>Deploying the AWS API Gateway V2 to LocalStack</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 02:06:00 +0000</pubDate>
      <link>https://dev.to/darkedges/deploying-the-aws-api-gateway-v2-to-localstack-49ab</link>
      <guid>https://dev.to/darkedges/deploying-the-aws-api-gateway-v2-to-localstack-49ab</guid>
      <description>&lt;p&gt;We are getting close to being able to test our solution, the final piece is the deployment of an AWS API Gateway v2 with an Authorizer that trusts Auth0 tokens.&lt;/p&gt;

&lt;p&gt;For this we will create a number of AWS Configurations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway V2 will connect to the Fargate API Instance we deployed previously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a lot of configuration items so they are avaiable in the following branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git switch apiv2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First we will plan and then apply the changes if that is green, by using the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
terraform apply --auto-approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it has been deployed you should get the following output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alb = "http://alb.elb.localhost.localstack.cloud:4566/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use this address in a future request to confirm the API has been deployed correctly.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Deploying a custom API via AWS Fargate</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 02:01:06 +0000</pubDate>
      <link>https://dev.to/darkedges/deploying-api-via-aws-fargate-4noi</link>
      <guid>https://dev.to/darkedges/deploying-api-via-aws-fargate-4noi</guid>
      <description>&lt;p&gt;Now we have a Private Application Load Balancer deployed we can look at putting an API behind it. For this we will use an existing container image &lt;code&gt;darkedges/providerapi:1.0.2&lt;/code&gt; which provides a basic API that we are going to serve via an AWS API Gateway V2 protected by an Auth0 Access Token.&lt;/p&gt;

&lt;p&gt;For this we will create a number of AWS Configurations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECS Cluster as our Fargate service.&lt;/li&gt;
&lt;li&gt;ECS Service Task to deploy the custom API Container&lt;/li&gt;
&lt;li&gt;ALB updates to link it al.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a lot of configuration items so they are avaiable in the following branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git switch fargate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First we will plan and then apply the changes if that is green, by using the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
terraform apply --auto-approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it has been deployed you should get the following output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alb = "http://alb.elb.localhost.localstack.cloud:4566/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use this address in a future request to confirm the API has been deployed correctly.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Deploying an Private ALB to LocalStack</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Sat, 16 Aug 2025 00:49:39 +0000</pubDate>
      <link>https://dev.to/darkedges/deploying-an-alb-to-localstack-4b01</link>
      <guid>https://dev.to/darkedges/deploying-an-alb-to-localstack-4b01</guid>
      <description>&lt;p&gt;In the previous articles we stood up LocalStack and configured Terraform to plan a deployment. Next we will deploy an ALB to the platform and get its address so that we can use it in the next time.&lt;/p&gt;

&lt;p&gt;For this we will create a number of AWS Configurations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC to deploy into.&lt;/li&gt;
&lt;li&gt;Networking to allow the ALB to be deployed and connect to the ECS instance.&lt;/li&gt;
&lt;li&gt;SecurityGroups to manage all the network egress / ingress between services.&lt;/li&gt;
&lt;li&gt;ALB the Application Load Balancer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a lot of configuration items so they are avaiable in the following branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git switch alb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First we will plan and then apply the changes if that is green, by using the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
terraform apply --auto-approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it has been deployed you should get the following output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alb = "http://alb.elb.localhost.localstack.cloud:4566/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use this address in a future request to confirm the API has been deployed correctly.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Configuring Terraform to deploy into LocalStack</title>
      <dc:creator>DarkEdges</dc:creator>
      <pubDate>Fri, 15 Aug 2025 23:44:20 +0000</pubDate>
      <link>https://dev.to/darkedges/configuring-terraform-to-deploy-into-localstack-9jk</link>
      <guid>https://dev.to/darkedges/configuring-terraform-to-deploy-into-localstack-9jk</guid>
      <description>&lt;p&gt;From our previous article we have successfuly registered and deployed LocalStack. Now we are planning to deploy our API onto LocalStack using AWS Fargate via Terraform, but we need to deploy some supporting services first, such as the core Terraform configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;

&lt;p&gt;We are going to be using terraform to manage our state so lets create the &lt;code&gt;terraform&lt;/code&gt; directory and place the following file &lt;code&gt;_provider.tf&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;provider "aws" {
  region     = "us-east-1"
  access_key = "test"
  secret_key = "test"

  endpoints {
    acm                      = "http://localhost:4566"
    amplify                  = "http://localhost:4566"
    apigateway               = "http://localhost:4566"
    apigatewayv2             = "http://localhost:4566"
    appconfig                = "http://localhost:4566"
    applicationautoscaling   = "http://localhost:4566"
    appsync                  = "http://localhost:4566"
    athena                   = "http://localhost:4566"
    autoscaling              = "http://localhost:4566"
    backup                   = "http://localhost:4566"
    batch                    = "http://localhost:4566"
    cloudformation           = "http://localhost:4566"
    cloudfront               = "http://localhost:4566"
    cloudsearch              = "http://localhost:4566"
    cloudtrail               = "http://localhost:4566"
    cloudwatch               = "http://localhost:4566"
    cloudwatchlogs           = "http://localhost:4566"
    codecommit               = "http://localhost:4566"
    cognitoidentity          = "http://localhost:4566"
    cognitoidp               = "http://localhost:4566"
    config                   = "http://localhost:4566"
    costexplorer             = "http://localhost:4566"
    docdb                    = "http://localhost:4566"
    dynamodb                 = "http://localhost:4566"
    ec2                      = "http://localhost:4566"
    ecr                      = "http://localhost:4566"
    ecs                      = "http://localhost:4566"
    efs                      = "http://localhost:4566"
    eks                      = "http://localhost:4566"
    elasticache              = "http://localhost:4566"
    elasticbeanstalk         = "http://localhost:4566"
    elasticsearch            = "http://localhost:4566"
    elb                      = "http://localhost:4566"
    elbv2                    = "http://localhost:4566"
    emr                      = "http://localhost:4566"
    events                   = "http://localhost:4566"
    firehose                 = "http://localhost:4566"
    glacier                  = "http://localhost:4566"
    glue                     = "http://localhost:4566"
    iam                      = "http://localhost:4566"
    iot                      = "http://localhost:4566"
    iotanalytics             = "http://localhost:4566"
    iotevents                = "http://localhost:4566"
    kafka                    = "http://localhost:4566"
    kinesis                  = "http://localhost:4566"
    kinesisanalytics         = "http://localhost:4566"
    kinesisanalyticsv2       = "http://localhost:4566"
    kms                      = "http://localhost:4566"
    lakeformation            = "http://localhost:4566"
    lambda                   = "http://localhost:4566"
    mediaconvert             = "http://localhost:4566"
    mediastore               = "http://localhost:4566"
    neptune                  = "http://localhost:4566"
    organizations            = "http://localhost:4566"
    qldb                     = "http://localhost:4566"
    rds                      = "http://localhost:4566"
    redshift                 = "http://localhost:4566"
    redshiftdata             = "http://localhost:4566"
    resourcegroups           = "http://localhost:4566"
    resourcegroupstaggingapi = "http://localhost:4566"
    route53                  = "http://localhost:4566"
    route53resolver          = "http://localhost:4566"
    s3                       = "http://s3.localhost.localstack.cloud:4566"
    s3control                = "http://localhost:4566"
    sagemaker                = "http://localhost:4566"
    secretsmanager           = "http://localhost:4566"
    serverlessrepo           = "http://localhost:4566"
    servicediscovery         = "http://localhost:4566"
    ses                      = "http://localhost:4566"
    sesv2                    = "http://localhost:4566"
    sns                      = "http://localhost:4566"
    sqs                      = "http://localhost:4566"
    ssm                      = "http://localhost:4566"
    stepfunctions            = "http://localhost:4566"
    sts                      = "http://localhost:4566"
    swf                      = "http://localhost:4566"
    timestreamwrite          = "http://localhost:4566"
    transfer                 = "http://localhost:4566"
    waf                      = "http://localhost:4566"
    wafv2                    = "http://localhost:4566"
    xray                     = "http://localhost:4566"
  }

  default_tags {
    tags = {
      Environment = "Local"
      Service     = "LocalStack"
    }
  }
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.100.0"
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can test it is working by performing the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform init
terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all went well we should see&lt;/p&gt;

&lt;h2&gt;
  
  
  terraform init
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "6.9.0"...
- Installing hashicorp/aws v6.9.0...
- Installed hashicorp/aws v6.9.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  terraform plan
&lt;/h2&gt;



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

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
  </channel>
</rss>
