OBO Token Exchange
RFC 8693 delegated authentication for agent-to-service calls
OBO (On Behalf Of) Token Exchange
Comprehensive guide to the platform's RFC 8693-compliant token exchange mechanism, which enables agents to act on behalf of users when calling downstream services.
Overview
What Is OBO Token Exchange?
On Behalf Of (OBO) token exchange allows the platform to obtain scoped, short-lived tokens from the identity provider (Keycloak) so that agents can call downstream services (SAP, Salesforce, ServiceNow, etc.) as the user rather than as a generic service account. This preserves the user's identity and authorization context through the entire call chain.
Why Is It Needed?
Enterprise systems enforce their own authorization. When an agent calls SAP S/4HANA to retrieve purchase orders, SAP needs to know which user is requesting the data so it can enforce SAP-level entitlements. A generic service account would either have too much access (security risk) or too little (broken functionality). OBO solves this by exchanging the user's JWT for a delegated token scoped to the target system.
RFC 8693 Compliance
The implementation follows RFC 8693 (OAuth 2.0 Token Exchange). The key parameters:
| RFC 8693 Parameter | Platform Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | User's JWT from auth context |
subject_token_type | urn:ietf:params:oauth:token-type:access_token |
actor_token | Agent's JWT-SVID (when available) |
actor_token_type | urn:ietf:params:oauth:token-type:jwt |
audience | Target service identifier (e.g., sap-s4hana) |
scope | Constrained scopes for the delegated token |
Supported Exchange Flows
| Flow | Use Case | IdP Requirement |
|---|---|---|
| RFC 8693 (generic) | Keycloak token exchange for any OIDC-capable downstream service | Keycloak with Token Exchange SPI enabled |
| SAP Destination Service | OAuth2UserTokenExchange for SAP BTP systems | SAP BTP Destination Service + Keycloak |
Use Cases
- SAP S/4HANA -- Agent reads purchase orders, creates invoices as the user
- Salesforce -- Agent queries CRM records respecting field-level security
- ServiceNow -- Agent creates/updates tickets under the user's identity
- Microsoft Graph -- Agent accesses SharePoint/Teams resources with user consent
- Custom APIs -- Any OAuth2-protected internal service
Architecture
End-to-End Sequence
Scroll to zoom · Drag to pan · 100%
Component Responsibilities
Scroll to zoom · Drag to pan · 100%
Delegation Chains
When Agent A delegates to Agent B, the OBO token must be forwarded through the chain. The platform tracks this via the delegationChain array and the X-Agent-Actor header.
User to Agent A to Agent B
Scroll to zoom · Drag to pan · 100%
Delegation Depth Control
Scroll to zoom · Drag to pan · 100%
The platform enforces configurable delegation depth limits via the maxDelegationDepth setting on interaction policies. The delegationChain array grows with each hop. When delegationChain.length >= maxDelegationDepth, further delegation is blocked.
Default depth limits by workflow type:
| Workflow Type | Default Max Depth |
|---|---|
| Simple agent | 3 |
| Multi-agent orchestration | 5 |
| SRE incident response | 4 |
Security Model
Encryption at Rest
All cached OBO tokens are encrypted using AES-256-GCM before being stored in the tenantCredentials table. The encryption is handled by encryptCredential() and decryptCredential() from the credential resolver module.
Scroll to zoom · Drag to pan · 100%
SPIFFE Identity
Each agent has a SPIFFE ID: spiffe://{trustDomain}/tenant/{tenantId}/agent/{agentId}. This identity is used as the actor_token in the RFC 8693 exchange when available as a JWT-SVID, establishing a cryptographic proof of which agent is acting on behalf of the user.
Scoped Delegation
OBO tokens are scoped in three ways:
- Audience -- The token is only valid for the specified target service
- Scopes -- Only the requested scopes are granted (e.g.,
read:ordersbut notwrite:orders) - Time -- Tokens have a short TTL set by Keycloak
Cache Key Derivation
Cache keys are derived using SHA-256 to prevent collisions and information leakage:
cacheKey = SHA-256(userId + ":" + agentId + ":" + audience + ":" + sortedScopes).slice(0, 32)
This ensures that different user/agent/audience/scope combinations never share cached tokens.
60-Second Expiry Buffer
When checking cached tokens, the platform subtracts 60 seconds from the expiry time. This prevents race conditions where a token that is technically valid could expire between retrieval and use.
Audit Logging
Every OBO token exchange is audit-logged with:
- Agent and user identifiers
- Target audience and scopes
- Delegation chain depth
- Risk level (low for direct, medium for chain depth 1-2, high for depth 3+)
Admin Dashboard
OBO Token Monitoring
The Admin Console provides real-time visibility into OBO token activity.
Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/admin/obo/tokens | GET | List all active OBO tokens with optional agentId and audience filters |
/api/admin/obo/tokens/:agentId | GET | List OBO tokens for a specific agent |
/api/admin/obo/stats | GET | Token exchange statistics (active/expired/revoked counts, per-agent breakdown) |
/api/admin/obo/revoke | POST | Revoke OBO tokens for a specific agent (and optionally a specific user) |
Stats Response Structure
{
"data": {
"totalActive": 12,
"totalExpired": 45,
"totalRevoked": 3,
"byAgent": [
{ "agentId": "agent-finance-1", "count": 8 },
{ "agentId": "agent-sap-reader", "count": 4 }
]
}
}
Bulk Revocation
Admins can revoke all OBO tokens for an agent (e.g., after a security incident):
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"agentId": "agent-finance-1"}' \
http://localhost:4000/api/admin/obo/revoke
To revoke tokens for a specific user-agent pair:
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"agentId": "agent-finance-1", "userId": "user-jane-1"}' \
http://localhost:4000/api/admin/obo/revoke
Configuration
Agent oboTargets
Agents declare their OBO requirements in the config.oboTargets array. This is configured in the Studio Agent Designer (Safety tab) or via the API.
{
"name": "Finance Agent",
"config": {
"oboTargets": [
{
"audience": "sap-s4hana",
"scopes": ["read:orders", "write:invoices"],
"description": "Access SAP purchase orders and invoices",
"type": "rfc8693"
},
{
"audience": "salesforce",
"scopes": ["api", "refresh_token"],
"description": "Query Salesforce CRM records",
"type": "rfc8693"
}
]
}
}
OBOTarget Schema
| Field | Type | Required | Description |
|---|---|---|---|
audience | string | Yes | Target service identifier (e.g., sap-s4hana) |
scopes | string[] | Yes | OAuth scopes to request |
description | string | No | Human-readable purpose |
type | rfc8693 or sap-destination | No | Exchange flow type (default: rfc8693) |
sapDestinationName | string | No | SAP BTP Destination Service name (SAP flow only) |
Keycloak Setup
Keycloak must be configured to support token exchange:
- Enable Token Exchange -- The realm must have the Token Exchange SPI enabled
- Create Confidential Client -- A client for the API Gateway with
client_credentialsandtoken-exchangegrant types - Configure Audience -- Register each downstream service as a client/resource in Keycloak
- Map Scopes -- Create scope mappings between the platform client and target service scopes
- Set Policies -- Define Token Exchange policies that allow the platform client to exchange tokens for the target audiences
Environment Variables
| Variable | Required | Description |
|---|---|---|
OBO_TOKEN_ENDPOINT | Yes | Keycloak token endpoint (e.g., https://keycloak.example.com/realms/platform/protocol/openid-connect/token) |
OBO_CLIENT_ID | Yes | Platform's confidential client ID |
OBO_CLIENT_SECRET | Yes | Client secret (store in Vault for production) |
SPIFFE_TRUST_DOMAIN | No | SPIFFE trust domain (default: agentic-platform) |
How Tokens Are Delivered to Agents
When the External Agent Executor calls an agent that has oboTargets, it sets HTTP headers:
X-OBO-Token-sap-s4hana: Bearer <delegated-token>
X-OBO-Token-salesforce: Bearer <delegated-token>
X-Agent-Actor: {"sub":"spiffe://agentic-platform/tenant/t1/agent/a1","client_id":"api-gateway","delegation_chain":[]}
The agent reads the X-OBO-Token-{audience} header and uses it when calling the corresponding downstream service.
Key Source Files
| File | Description |
|---|---|
services/api-gateway/src/lib/obo-token-exchange.ts | Core OBO exchange, caching, revocation, and stats |
services/api-gateway/src/middleware/obo-context-middleware.ts | Extracts user token for downstream OBO exchange |
services/api-gateway/src/lib/external-agent-executor.ts | Sets X-OBO-Token-* headers when calling agents |
services/api-gateway/src/routes/admin.ts | Admin monitoring and revocation endpoints |
packages/types/src/agent.ts | OBOTarget and OBOTargetType type definitions |
apps/studio/app/(main)/agents/components/safety-tab.tsx | Studio UI for configuring oboTargets |
services/api-gateway/src/lib/schemas/agent-config.schema.ts | Zod validation for oboTargets |