Token exchange
This guide shows how IDENTOS PBAC supports agentic delegation chains using the OAuth 2.0 Token Exchange grant (RFC 8693).
Token exchange is implemented: subject token validation, scope narrowing, resource resolution, and cross-domain id_token exchange all work today. The actor_token parameter (for composite tokens with act.sub) is planned. See the RFC 8693 compliance page for detailed status.
Overview
When an AI agent acts on behalf of a human user and needs to delegate work to another specialized agent, a plain access token is insufficient — it loses the chain of custody. Token exchange solves this with a composite token that carries:
sub— the original human whose data is being accessedact.sub— the agent that is acting (the delegator)aud— the downstream agent the token is issued toscp— scopes down-scoped to exactly what the downstream agent needs
OPA validates the entire delegation chain before issuing the composite token.
API flow: the agentic delegation chain
1. Registration (agent startup)
Before an agent can do anything, it needs an identity. It uses Dynamic Client Registration (DCR) with a signed statement from your internal "Agent Registry."
- Call:
POST /register - Payload:
{ "software_statement": "JWT_SIGNED_BY_REGISTRY" } - Result: The Agent now has a
client_id(Agent_A) and a secret (or private key).
2. Initial delegation (human to Agent A)
The human user authorizes Agent A to start the workflow (e.g., "Analyze my Q1 spending").
- Call:
GET /authorize?response_type=code&client_id=Agent_A&scope=openid financial:read - Process: Human logs in via OIDC.
- Result: Agent A receives an
authorization_code.
3. Getting the subject token
Agent A exchanges the code for tokens. This creates the Subject Token (the human's context).
- Call:
POST /token?grant_type=authorization_code&code=XYZ&client_id=Agent_A - Result:
AT_Userwithsub: "human_123"— this is the Subject Token.
4. Agent A gets its own identity (the actor token)
Agent A authenticates as itself to obtain a token representing its own identity.
- Call:
POST /token?grant_type=client_credentials&client_id=Agent_A&scope=agent:internal - Result:
AT_Agent_Awithsub: "Agent_A"— this is the Actor Token.
5. The token exchange (Agent A to Agent B)
Agent A needs a specialized "PDF Parser Agent" (Agent B) to read receipts. It calls the Auth Server to delegate:
- Call:
POST /token - Payload:
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "AT_User",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token": "AT_Agent_A",
"actor_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "Agent_B",
"scope": "pdf:read"
}
OPA validation: The Rego policy checks whether Agent A is allowed to delegate pdf:read to Agent B for user human_123.
Result: The AS issues a Composite Token for Agent B.
Final data state: the composite token
The token Agent B receives carries the full delegation chain, enabling precise audit logs.
{
"sub": "human_123",
"aud": "Agent_B",
"iss": "https://your-auth-server.com",
"exp": 1741250000,
"scp": ["pdf:read"],
"act": {
"sub": "Agent_A"
}
}
If Agent B attempts an operation beyond pdf:read, introspection returns the exact chain of who authorized what.
Summary of state per step
| Step | Token Held | Identity Represented |
|---|---|---|
| Step 3 | AT_User | Human only |
| Step 4 | AT_Agent_A | Agent A only (the workload) |
| Step 5 | Composite Token | Human (Subject) + Agent A (Actor) |
Cross-domain identity via id_token exchange
Token exchange also supports id_token as the subject token type, enabling cross-domain identity flows aligned with draft-ietf-oauth-identity-chaining.
In this flow, a user authenticates at one authorization server (the "issuing AS"), and the resulting id_token is presented to a different AS (the "receiving AS") to obtain an access token. The receiving AS verifies the id_token signature against the issuing AS's JWKS and evaluates the subject's claims via OPA policy.
Example: cross-province healthcare access
A physician authenticated at Province A's AS needs to access a patient record at Province B's clinical data repository.
1. Province A issues an id_token for the physician (via standard authorization_code flow):
{
"iss": "https://province-a.pbac.example.com",
"sub": "dr-smith-12345",
"province": "A",
"specialty": "emergency_medicine",
"college": "CPSA",
"college_status": "active"
}
2. The physician's EMR (already DCR'd with Province B) presents the id_token:
curl -s -X POST https://province-b.pbac.example.com/token \
-u "emr-client-id:secret" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=<id_token_jwt>" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:id_token"
3. Province B's AS:
- Verifies the JWT signature against Province A's JWKS (
{iss}/.well-known/jwks.json) - Checks that Province A is in
trusted_issuers(via OPA token policy) - Extracts subject claims (
province,specialty, etc.) and passes them to OPA - OPA evaluates access — may attach obligations (e.g., redact sensitive data for out-of-province providers)
- Returns an access token scoped to Province B's resources
Prerequisites
- Province A's issuer URL must be in Province B's
data.oauth.trust.trusted_issuers— see Trust Issuers - Province A must serve its public key at
{issuer}/.well-known/jwks.json - The requesting client must have
urn:ietf:params:oauth:grant-type:token-exchangein itsgrant_types
Key differences from access_token exchange
| access_token exchange | id_token exchange | |
|---|---|---|
| Subject token source | Issued by this AS | Issued by a trusted external AS |
| Verification | Database lookup | JWT signature + JWKS |
| Scope narrowing | New scopes must be subset of original | No scope constraint — client entitlements apply |
| Trust model | Implicit (our own token) | Explicit (trusted_issuers list) |
| Use case | Delegation chains (agent-to-agent) | Cross-domain identity (federated access) |
Policy design notes
The OPA policy for token exchange should verify:
- The subject token (
AT_User) is valid and belongs to a real user - The actor token (
AT_Agent_A) is valid and identifies a registered agent - Agent A is authorized to delegate the requested scopes to Agent B
- The audience (
Agent_B) is a registered client - The requested scopes are a subset of what the subject token already grants
For id_token exchange, the default token.rego policy additionally checks that the id_token's issuer is in trusted_issuers. Custom policies can use the subject's claims (e.g., province, specialty) to attach obligations or restrict access.
See the Policy Data Configuration guide for how to express client entitlements and trust issuers.
Next steps
- AI Agents Use Case — Trust tiers, MCP server protection, and agent authorization patterns
- MCP Gateway — Protect MCP servers with policy-enforced token introspection
- Registering Software — How agents register with software statements via DCR