[Design Proposal] Agent Identity & Access Control #635
Replies: 3 comments
-
|
Thorough design proposal. WSO2's position in the identity stack makes this especially impactful — if agent-manager ships native agent IAM, it immediately reaches WSO2's existing enterprise footprint. The gap analysis is spot-on: OAuth 2.0 treats agents as static clients, but agents are dynamic, autonomous, and cross-boundary. The delegation chain problem you describe (Agent A→B→C with attenuation) is being actively discussed in the MCP core repo as well. Two observations from production: 1. Cross-org is the hard part. Everything in this proposal works well within WSO2's control plane. The challenge is: what happens when an agent on WSO2-managed infra calls a tool hosted by a different org using a different identity provider? No shared directory, no admin relationship. This is where self-sovereign agent identity (cryptographic, verifiable by any party independently) becomes necessary. 2. Trust ≠ Authentication. Even with perfect agent auth, the tool provider needs to answer: should I trust this agent? Not just is this agent authenticated? Trust requires history — what has this agent done before, has it behaved reliably? Authentication is binary; trust is a spectrum. We built SATP (Solana Agent Trust Protocol) to address both gaps:
Would WSO2 agent-manager consider supporting pluggable external trust providers? The architecture you describe could natively integrate external trust signals alongside internal policy evaluation. |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for the thoughtful feedback, @0xbrainkid. Really appreciate you taking the time to engage deeply with the proposal and sharing your production experience. You're raising two critical points, and I want to address both directly.
You're absolutely right — everything within a single organization's control plane is the "easier" problem. The real challenge emerges when agents cross organizational boundaries with no shared directory or admin relationship. This is something we're actively thinking about. Our current approach is to solve this through trusted authorization servers and federated users, essentially allowing organizations to establish trust relationships where each org's authorization server can recognize and validate agent identities from the other. But I'll be honest: this is one of the hardest problems in the space, and we don't claim to have it fully figured out. Your point about cryptographic, self-sovereign agent identity that any party can verify independently is well taken — that's a compelling direction for the truly decentralized case.
This is an important distinction. Authentication tells you who the agent is; trust tells you whether you should let it in. We see trust as a spectrum too, not a binary gate. On SATP and pluggable trust providers: Yes — modularity is a core design goal for us. We want agent-manager to support pluggable identity providers, gateways, and policy engines. The idea of pluggable external trust providers is very interesting. We'll take a closer look at SATP to understand how trust signals like on-chain verifiable history could integrate alongside our internal policy evaluation. Where exactly trust providers fit in the architecture is something we need to explore further, but it's clearly a relevant dimension — especially for cross-org scenarios where no single authority can vouch for an agent's behavior. Thanks again for pointing us to SATP and the MCP discussion thread — we'll dig into both. |
Beta Was this translation helpful? Give feedback.
-
|
Identity and access control for agents is necessary but not sufficient. Access control answers "can this agent use this tool?" — but it doesn't answer "is this agent using this tool within acceptable parameters?" An agent with permission to call a transfer API can still move $50K when it should be capped at $500. Access control permits the tool call; behavioral constraints govern how it's used. I've been building this as a complementary layer: Nobulex. Agents declare behavioral rules (permit/forbid/require with conditions), and every action gets evaluated against those rules at runtime — before execution. Violations are blocked, not logged after the fact. Every decision goes into a SHA-256 hash-chained log for independent verification. The two layers compose:
Playground: nobulex.com/playground |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Problem
AI agents on our platform need to call external tools (APIs, MCP servers) on behalf of users. Today, there is no standardized way for agents to prove their identity, access tools securely, or act on behalf of users with proper delegation. This is not unique to us. It is the central challenge the industry is converging on.
The core gap: Traditional IAM (OAuth 2.0, SAML) was designed for human users and static service accounts, not for autonomous systems that reason about goals, make independent decisions, chain delegations across domains, and may be created and destroyed dynamically [CSA][OpenID]. Treating agents as plain OAuth2 clients or service accounts leaves critical gaps:
Industry consensus: Agents must be treated as first-class identities with the same provisioning, credential rotation, access control, and deprovisioning expected for human employees (OWASP), but with identity models designed for autonomy, delegation, and scale (CoSAI, OpenID Foundation).
Functional Goals
Security Principles
Aligned with CoSAI's Principles for Secure-by-Design Agentic Systems [CoSAI] and OWASP's Agentic Application Security Guide [OWASP]:
User Stories
As a platform engineer, I want to register an agent as a non-human identity with a designated human owner and have its credentials auto-injected at deploy time so that agents are first-class, accountable identities in the directory with minimal developer effort.
As an agent developer, I want my agent to obtain a per-tool OBO token (binding agent + user identity) for each tool call so that it can act on behalf of the authenticated user with scoped, auditable delegation.
As a platform engineer, I want to register HTTP APIs and MCP servers as tools (with URL patterns, auth types, scopes, and credentials stored in a secret store), bind them to agents, and configure per-tool consent policies so that I control what each agent can access and under what conditions.
As a platform engineer, I want the gateway to enforce tool access through agent identity and OBO tokens with least-privilege scopes, rather than exposing raw credentials to agents, so that a compromised agent cannot access tools beyond its current task or escalate beyond the delegating user's permissions.
As an agent developer, I want agent-friendly consent flows (inline 401 + auth_url for tool connections, out-of-band approval for high-risk actions, connection management APIs) so that my agent can acquire and manage user consent as part of the natural conversation flow [4][5][8][9][12][13].
As a platform engineer, I want to suspend or revoke an agent's identity with immediate effect so that I can contain anomalous agent behavior in real time.
As a security auditor, I want a full audit trail of token issuance, tool access, and consent changes, plus usage analytics to detect authorization drift, so that I can investigate incidents, prove compliance, and right-size permissions [23][26][28].
Existing Solutions
Agent Identity Models
Platforms with Real Agent Identity
GetWorkloadAccessTokenForJWTAPI (not RFC 8693). Token cached in AgentCore Token Vault.Platforms without Real Agent Identity
entity_idstring (defaults to "default") [36]connection_idstring [38]user_idstring /Arcade-User-IDheader [39]Token Flow Patterns — Three Industry Approaches
Every platform requires the agent developer to write some code to forward user identity on outbound tool calls. The patterns differ in how much code and where the credential exchange happens.
Pattern 1: Platform Proxy +
user_idString (Composio, Nango, Arcade)The agent never calls the tool API directly. All calls go through the platform's managed proxy, which injects stored credentials server-side. The developer passes a
user_idstring on every SDK call.Agent identity: Just a developer API key (identifies the org, NOT the agent). The downstream API has no knowledge of which agent called.
sequenceDiagram participant U as End User participant A as Agent App participant P as Composio/Arcade<br/>(managed proxy) participant T as Tool API<br/>(e.g. Google) U->>A: "Check my email" Note over A: Agent passes user_id on every call A->>P: execute(tool="Gmail.ListEmails",<br/>user_id="user-123") P->>P: lookup stored OAuth token<br/>for (user-123, Gmail) P->>T: GET gmail.googleapis.com<br/>Auth: Bearer user's_gmail_token T-->>P: emails P-->>A: result A-->>U: "You have 3 new emails"Pros: Zero boilerplate. Just pass a
user_id. No OAuth logic in agent code.Cons: No real agent identity. Downstream can't distinguish which agent called. Developer API key is shared across all agents.
Pattern 2: Token Exchange + Decorator (Auth0/Okta, AWS AgentCore)
The agent has a real OAuth2 identity. It exchanges its own credentials + the user's inbound JWT for an OBO/workload token. This is typically wrapped in a decorator or SDK call.
sequenceDiagram participant U as End User participant IG as Ingress Gateway participant A as Agent App participant IDP as IDP<br/>(Auth0/AgentCore) participant T as Tool API<br/>(e.g. Google) U->>IG: request (with user JWT) IG->>A: forward + user JWT Note over A: @requires_access_token decorator fires A->>IDP: token exchange<br/>(agent_creds + user_jwt) IDP-->>A: tool-specific token<br/>(user's Google access_token) A->>T: GET gmail.googleapis.com<br/>Auth: Bearer user's_google_token T-->>A: emails A-->>IG: response IG-->>U: responsePros: Real agent identity. Token Vault enforces which agents can access which users' tokens.
Cons: More integration code (decorators, SDK calls). Developer must extract user token from inbound request.
Pattern 3: Sidecar (Microsoft Entra AgentID SDK)
A containerized companion service runs alongside the agent and handles all OBO token exchange. The agent forwards the user's inbound
Authorizationheader to the sidecar, which does the rest. Currently in Preview [15].sequenceDiagram participant U as End User participant IG as Ingress Gateway participant A as Agent App participant S as Sidecar<br/>(Entra SDK) participant E as Entra ID participant T as Tool API<br/>(e.g. MS Graph) U->>IG: request IG->>A: forward + user JWT Note over A: Extract Authorization header Note over A: LLM decides to call tool A->>S: GET /DownstreamApi/Graph<br/>Auth: <user JWT> S->>E: OBO exchange<br/>agent_cert + user_token E-->>S: OBO token S->>T: GET /me<br/>Auth: Bearer <OBO token> T-->>S: profile S-->>A: result A-->>IG: response IG-->>U: responsePros: Agent developer writes simple HTTP calls, no OAuth logic. Sidecar handles caching, refresh, and downstream calls.
Cons: Requires a companion container. Developer still writes code to forward user's
Authorizationheader.Pattern Summary
user_idstringactclaim / workload binding)user_idper callAuthorizationheaderConsent & Authorization Patterns
How Platforms Handle Consent
OAuth Intermediary / Token Vault Comparison
/login/callback)/api/v1/callback)/oauth/callback)/oauth/callback)Gateway & MCP Ecosystem
How Platforms Route Agent-to-Tool Traffic
Platforms take one of three approaches to routing agent traffic to tools: a shared gateway (one per org/account, all agents connect to it), a per-pod sidecar (one per application instance, runs alongside the agent), or a managed SaaS proxy (vendor-hosted, agents call a cloud API).
connectionId. Nango resolves the provider, retrieves/refreshes the user's OAuth token, injects credentials, and forwards the request. REST-to-REST proxy, no MCP support.localhost(sidecar). The sidecar handles all Entra ID interactions: token validation, OBO exchange, credential caching, and optionally proxies downstream API calls with tokens attached. It is NOT a shared service, each pod runs its own instance.MCP Multi-Tenancy — How Platforms Handle Multiple Users
https://backend.composio.dev/v3/mcp/SERVER_ID?user_id=USER_ID. URL encodes user context.Authorization: Bearertoken orArcade-User-IDheader on every request.McpAgentcompute instance (extends Durable Object). Natural isolation.Security Principles Coverage
How well each platform enforces the four security principles for agent identity and delegated access:
knownClientApplications; no silent delegation pathwayeffective permissions = identity policy ∩ permission boundary; RFC 8693 scope cannot exceed the subject token's granted scopesentity_idis an app-level string; no directory ownership; developer API key identifies org, not individual agentconnection_idis a developer-assigned string; org-level Secret Key only; no accountability chain to a named humanOAuth2(scopes=[...])at tool definition time — better granularity than Composio/Nango, but still static per toolwaitForCompletion()and cannot proceed without active user actionLegend: ✅ Formally enforced⚠️ Partially supported / depends on configuration ❌ Not enforced
Key differentiators:
Proposed Solution
Overview
Each agent gets a non-human identity (NHI) in Thunder. This is a first-class entity with both user nature (directory entry, attributes, roles, groups) and app nature (OAuth2 client for authentication). The same pattern is used by Okta (NHI in Universal Directory), Microsoft Entra (Agent Identity Blueprint), and AWS (Workload Identity).
At runtime, the agent uses RFC 8693 token exchange to obtain a per-tool OBO token (binding agent + user identity) and sends it to the API Gateway, which validates locally (cached JWKS), checks scope, and translates to whatever credential the downstream tool requires. The downstream tool never sees the OBO token.
We follow Pattern 2 (Token Exchange) with a key simplification: a single OBO token per tool call, validated and translated at the gateway. This matches AWS AgentCore and Auth0's approach.
Design
Agent Identity — NHI in Thunder
An agent identity is NOT just an OAuth2 client. It's a non-human identity (NHI), a first-class entity in the identity system, like a human user but representing an agent.
What gets created in Thunder when an agent is registered:
Core Design — OBO Token + Gateway
Key design decisions:
scope: "tools:twilio"). Tokens are cached per (user, tool) pair in the SDK for the session. This matches the industry pattern: AWS@requires_access_tokenand Auth0withTokenForConnectionboth issue per-tool tokens lazily. Sends singleAuthorization: Bearer <OBO>to gateway per call. No separate headers.AGENT_CLIENT_ID/AGENT_CLIENT_SECRET). Agent uses these + user assertion to get OBO token from Thunder.x-jwt-assertionheader. Agent uses it as thesubject_tokenin the RFC 8693 exchange with Thunder./tools/twilio/*→api.twilio.com/*are shared across all agents. The gateway checks whether this agent is permitted to access this tool by inspecting the OBO token'sscopeclaim.scope: "tools:twilio"). Each OBO token carries only the scope for the tool being called. Thunder issues the token with the requested scope, validated against the agent's registered tool bindings and the user's entitlements. The gateway validates the JWT locally (cached JWKS, no per-request IDP call) and checks thescopeclaim against the requested tool.sub+actclaims)Component responsibilities:
scopeclaim for tool permission → translate to tool credential → proxy → audit logAuthorization Model
Permissions as scopes in the OBO token, validated locally at the gateway.
The industry standard is a three-layer model:
tools:twilioscope?scopeclaim in JWT (local string check)No gateway calls the IDP on every request. This is how Kong, Envoy/Istio, AWS API Gateway, WSO2 APIM, and Azure APIM all work: local JWT validation with cached JWKS.
Runtime authorization flow:
Four Security Principles
Any agent identity + delegated access design should guarantee four fundamental properties:
subis recorded at creation and stored as the agent'sownerattribute. Owners can suspend or revoke the agent at any time (Story 5, 6).scope: "tools:twilio"). Tokens are cached per (user, tool) for the session. The gateway rejects any call wheretoken.scope ∩ routeis empty. No agent ever carries a wildcard or multi-tool grant on a single token (Story 25).tools:foofor a user who has nofooentitlement (Story 16).auth_url(LLM-inline consent). High-risk actions trigger an out-of-band CIBA approval. There is no code path by which an agent silently escalates its own access (Stories 12, 14, 17).Why User JWT (not user_id) for OBO
Some platforms (Composio, Nango, Arcade) pass a plain
user_idstring instead of the user's actual JWT for identifying the user in token exchanges. This is a critical security distinction.expclaimsub(user) +act(agent) in signed OBO tokenWhy those platforms use user_id: Simpler integration — developers don't need to manage JWT forwarding. Their security model assumes the developer's backend has already authenticated the user. ZITADEL's documentation explicitly calls this approach "experimental and potentially insecure since trust is fully placed into the app making the request."
Our approach: The agent uses the user's actual JWT — received via
x-jwt-assertionfrom the ingress gateway — as thesubject_tokenin the RFC 8693 exchange. Thunder cryptographically verifies this JWT (signature, expiry, audience) before issuing the OBO token. A compromised agent cannot forge or replay a user's identity.This aligns with the emerging IETF standard draft-oauth-ai-agents-on-behalf-of-user, which mandates actual user tokens (not user_id strings) for OBO exchanges.
Credential Translation
The gateway is the authorization boundary. After validating the OBO token, it translates to whatever credential the downstream tool expects:
scopedoes not include requested toolsub+act)Approach Diagrams
The approach diagrams cover every combination of four architectural dimensions that shape the auth flow:
Scenario Matrix
The diagrams are grouped by privilege model:
client_credentials. No user login. Agent issub.client_credentialswithctx_callerclaim. Caller is logged in but agent uses its own entitlements. Caller identity is audit metadata only.Case 0: Agent's Own Privileges — No Caller, No Credential Translation
No human user. Agent is triggered by a scheduler, cron job, service, event, or CLI. Authenticates with
client_credentials, operates under its own entitlements. Tool is internal — gateway forwards JWT directly, no credential translation.sequenceDiagram participant C as Caller<br/>(Scheduler / Service /<br/>Event / CLI) participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant T as Internal Tool API<br/>(e.g., Analytics) Note over A: Deploy time: AGENT_CLIENT_ID,<br/>AGENT_CLIENT_SECRET injected as env vars C->>A: trigger request (no user context) A->>TH: POST /token<br/>grant_type: client_credentials<br/>client_id + client_secret<br/>scope: "tools:analytics" TH->>TH: Validate agent identity ✓<br/>Check agent's registered scopes ✓ TH-->>A: JWT {sub: "pipeline-agent",<br/>scope: "tools:analytics",<br/>aud: "tool-gateway"} Note over A: sub = agent.<br/>No user claims.<br/>Agent's own entitlements only. A->>TG: GET /tools/analytics/v1/report<br/>Authorization: Bearer <token> TG->>TG: Validate JWT locally (cached JWKS)<br/>✓ Signature, expiry, issuer, audience<br/>✓ scope contains "tools:analytics"<br/>Extract agent_id (no user) Note over TG: Tool is platform-managed.<br/>No credential translation needed. TG->>T: Forward request directly<br/>(service mesh call)<br/>JWT forwarded or agent_id as header T-->>TG: results TG-->>A: response A-->>C: results Note over TG: Audit log:<br/>agent=pipeline-agent, user=NONE,<br/>tool=analyticsKey points:
sub= agent. Privileges = agent's registered scopes in ThunderCase 1: Agent's Own Privileges — No Caller, Static Credential
Same as Case 0, but tool is external (e.g., Twilio with API key). Gateway translates credentials — retrieves API key from secret store and injects it. External tool never sees our JWT.
sequenceDiagram participant C as Caller<br/>(Scheduler / Service /<br/>Event / CLI) participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant SS as Secret Store participant T as External Tool API<br/>(e.g., Twilio) Note over A: Deploy time: AGENT_CLIENT_ID,<br/>AGENT_CLIENT_SECRET injected as env vars C->>A: trigger request (no user context) A->>TH: POST /token<br/>grant_type: client_credentials<br/>client_id + client_secret<br/>scope: "tools:twilio" TH-->>A: JWT {sub: "notifier-agent",<br/>scope: "tools:twilio",<br/>aud: "tool-gateway"} A->>TG: POST /tools/twilio/v1/messages<br/>Authorization: Bearer <token> TG->>TG: Validate JWT locally (cached JWKS)<br/>✓ scope "tools:twilio" ✓ Note over TG: Credential translation needed.<br/>External tool expects API key. TG->>SS: GET credential for<br/>(notifier-agent, twilio) SS-->>TG: {apiKey: "TWILIO_AUTH_TOKEN_xxx"} TG->>T: POST api.twilio.com/2010-04-01/Messages<br/>Authorization: Bearer TWILIO_AUTH_TOKEN_xxx T-->>TG: message SID + status TG-->>A: response A-->>C: results Note over TG: Audit log:<br/>agent=notifier-agent, user=NONE,<br/>tool=twilioKey difference from Case 0: Gateway performs credential translation via secret store. External tool never sees our JWT. API key is typically org-level or agent-level (not per-user).
Case 2: Agent's Own Privileges + Caller Context — No Credential Translation
Caller (human user or upstream agent) is present and authenticated. Agent operates under its own privileges — not the caller's. Caller identity is embedded as a custom claim (
ctx_userorctx_agent) for audit and data filtering only. Tool is internal — no credential translation.sequenceDiagram participant C as Caller<br/>(Human User or<br/>Upstream Agent) participant IG as Ingress Gateway participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant T as Internal Tool API<br/>(e.g., Analytics) Note over A: Deploy time: AGENT_CLIENT_ID,<br/>AGENT_CLIENT_SECRET injected as env vars alt Human user (browser) C->>IG: request (with session JWT) IG->>IG: Validate user JWT ✓ IG->>A: Forward + x-jwt-assertion header else Upstream agent (service call) C->>IG: request (with agent JWT) IG->>IG: Validate agent JWT ✓ IG->>A: Forward + x-jwt-assertion header end Note over A: Extract caller assertion<br/>from x-jwt-assertion header A->>TH: POST /token<br/>grant_type: client_credentials<br/>client_id + client_secret<br/>scope: "tools:analytics"<br/>caller_assertion: <caller_jwt> Note over TH: Verify caller_assertion<br/>(signature, expiry) ✓<br/>Extract caller identity.<br/>Embed as custom claim — NOT sub.<br/>Privileges = AGENT's scopes only.<br/>Caller's entitlements NOT checked. TH-->>A: JWT {sub: "analytics-agent",<br/>ctx_caller: "user-123 | agent-A",<br/>scope: "tools:analytics",<br/>aud: "tool-gateway"} Note over A: sub = agent (not caller).<br/>ctx_caller = audit context only.<br/>Agent uses its OWN privileges. A->>TG: GET /tools/analytics/v1/report<br/>Authorization: Bearer <token> TG->>TG: Validate JWT locally ✓<br/>scope "tools:analytics" ✓<br/>Extract agent_id + ctx_caller TG->>T: Forward request<br/>agent_id + ctx_caller as headers T-->>TG: results TG-->>A: response A->>IG: response IG-->>C: "Report generated..." Note over TG: Audit log:<br/>agent=analytics-agent,<br/>ctx_caller=user-123 (who triggered),<br/>tool=analyticsKey points:
ctx_caller— agent cannot forge identitysub= agent. Privileges = agent's scopes. Caller's entitlements NOT checked.ctx_calleris for audit trail and optional data filtering, not authorizationCase 3: Agent's Own Privileges + Caller Context — Static Credential
Same as Case 2, but tool is external. Gateway translates credentials via secret store.
sequenceDiagram participant C as Caller<br/>(Human User or<br/>Upstream Agent) participant IG as Ingress Gateway participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant SS as Secret Store participant T as External Tool API<br/>(e.g., Twilio) alt Human user C->>IG: request (with session JWT) IG->>A: Forward + x-jwt-assertion else Upstream agent C->>IG: request (with agent JWT) IG->>A: Forward + x-jwt-assertion end A->>TH: POST /token<br/>grant_type: client_credentials<br/>client_id + client_secret<br/>scope: "tools:twilio"<br/>caller_assertion: <caller_jwt> TH-->>A: JWT {sub: "notifier-agent",<br/>ctx_caller: "user-123 | agent-A",<br/>scope: "tools:twilio",<br/>aud: "tool-gateway"} A->>TG: POST /tools/twilio/v1/messages<br/>Authorization: Bearer <token> TG->>TG: Validate JWT locally ✓<br/>scope "tools:twilio" ✓<br/>Extract agent_id + ctx_caller TG->>SS: GET credential for<br/>(notifier-agent, twilio) SS-->>TG: {apiKey: "TWILIO_AUTH_TOKEN_xxx"} TG->>T: POST api.twilio.com/2010-04-01/Messages<br/>Authorization: Bearer TWILIO_AUTH_TOKEN_xxx T-->>TG: message SID + status TG-->>A: response A->>IG: response IG-->>C: "SMS sent ✓" Note over TG: Audit log:<br/>agent=notifier-agent,<br/>ctx_caller=user-123,<br/>tool=twilioKey difference from Case 2: Gateway performs credential translation. External tool never sees our JWT or caller identity.
Case 4: OBO (Caller's Privileges) — No Credential Translation
Caller (human user or upstream agent) is present. Agent acts on behalf of the caller via RFC 8693 token exchange — bounded by the caller's entitlements. Tool is internal, trusts gateway — no credential translation needed.
sequenceDiagram participant C as Caller<br/>(Human User or<br/>Upstream Agent) participant IG as Ingress Gateway participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant T as Internal Tool API<br/>(e.g., HR System) alt Human user C->>IG: "What's my PTO balance?"<br/>(with session JWT) IG->>A: Forward + x-jwt-assertion else Upstream agent (delegation chain) C->>IG: request (with agent-A JWT) IG->>A: Forward + x-jwt-assertion end Note over A: LLM reasons → call HR tool A->>TH: RFC 8693 token exchange<br/>grant_type: token-exchange<br/>subject_token: caller_assertion (JWT)<br/>actor_token: agent_token<br/>scope: "tools:hr-system" TH->>TH: Validate caller JWT (signature ✓)<br/>Validate agent has tools:hr-system ✓<br/>Caller has hr-system entitlement ✓<br/>scope = intersection(agent, caller, requested) TH-->>A: OBO token<br/>{sub: "user-123 | agent-A",<br/>act: {sub: "hr-agent"},<br/>scope: "tools:hr-system"} Note over A: sub = caller (not agent).<br/>act = agent. Privileges = CALLER's<br/>entitlements. Agent is bounded. A->>TG: GET /tools/hr-system/v1/pto<br/>Authorization: Bearer <OBO_token> TG->>TG: Validate OBO locally (cached JWKS)<br/>✓ Signature, expiry, audience<br/>✓ scope "tools:hr-system"<br/>Extract caller_id + agent_id TG->>T: Forward request<br/>caller_id + agent_id as headers T-->>TG: PTO balance data TG-->>A: response A->>IG: response IG-->>C: "You have 12 PTO days remaining" Note over TG: Audit log:<br/>caller=user-123, agent=hr-agent,<br/>tool=hr-system, action=GET /ptoKey differences from Cases 2–3 (caller context):
client_credentialssub= caller (not agent).act= agent. Privileges = caller's entitlements.When caller is an upstream agent: Creates a delegation chain. If agent-A calls agent-B, the OBO token has
sub: agent-A, act: {sub: agent-B}. agent-B is bounded by agent-A's entitlements.Case 5: OBO (Caller's Privileges) — Static Credential
Same as Case 4, but tool is external. Gateway translates credentials via secret store.
sequenceDiagram participant C as Caller<br/>(Human User or<br/>Upstream Agent) participant IG as Ingress Gateway participant A as Agent App participant TH as Thunder (IDP) participant TG as Tool Gateway participant SS as Secret Store participant T as External Tool API<br/>(e.g., Twilio) alt Human user C->>IG: "Send confirmation SMS"<br/>(with session JWT) IG->>A: Forward + x-jwt-assertion else Upstream agent C->>IG: request (with agent JWT) IG->>A: Forward + x-jwt-assertion end A->>TH: RFC 8693 token exchange<br/>subject_token: caller_assertion<br/>actor_token: agent_token<br/>scope: "tools:twilio" TH-->>A: OBO token<br/>{sub: "user-123 | agent-A",<br/>act: {sub: "comms-agent"},<br/>scope: "tools:twilio"} A->>TG: POST /tools/twilio/v1/messages<br/>Authorization: Bearer <OBO_token> TG->>TG: Validate OBO locally ✓<br/>scope "tools:twilio" ✓<br/>Extract caller_id + agent_id TG->>SS: GET credential for<br/>(comms-agent, twilio) SS-->>TG: {apiKey: "TWILIO_AUTH_TOKEN_xxx"} TG->>T: POST api.twilio.com/2010-04-01/Messages<br/>Authorization: Bearer TWILIO_AUTH_TOKEN_xxx T-->>TG: message SID + status TG-->>A: response A->>IG: response IG-->>C: "SMS sent ✓" Note over TG: Audit log:<br/>caller=user-123, agent=comms-agent,<br/>tool=twilioKey difference from Case 4: Gateway translates credentials. Note that even though the credential is a static API key (agent/org level), the OBO token ensures the caller's entitlements are checked at Thunder — the agent can only call Twilio if the caller has
tools:twilioentitlement.Case 6: OBO + Tool Authorization — Pre-auth (OAuth Dance)
Complete user session. The tool provider (e.g., Google Calendar) requires user-level OAuth authorization — without it, you cannot access the user's data. User connects their tool account via OAuth during onboarding, then chats with OBO-authenticated tool calls.
The tool authorization (OAuth dance) happens because the tool itself requires it — Google won't give you calendar data without a user-authorized token.
sequenceDiagram participant U as End User<br/>(Browser) participant UI as Agent App UI participant AM as Agent Manager participant TP as Tool Provider<br/>(e.g., Google OAuth) participant SS as Secret Store participant IG as Ingress Gateway participant A as Agent App<br/>(LLM) participant TH as Thunder (IDP) participant TG as Tool Gateway participant T as Tool API<br/>(e.g., Google Calendar) Note over U,UI: ═══ PHASE 1: USER LOGIN ═══ U->>UI: Open agent app in browser UI->>TH: Redirect to Thunder login TH-->>U: Login page U->>TH: Authenticate (username/password, SSO, etc.) TH-->>UI: Authorization code UI->>TH: Exchange code → tokens TH-->>UI: User JWT (session token) Note over U,UI: User is now logged in.<br/>Session JWT established. Note over U,UI: ═══ PHASE 2: TOOL AUTHORIZATION ═══<br/>═══ (Tool requires OAuth consent) ═══ Note over U,UI: Agent app shows Connected Accounts<br/>or first-use setup U->>UI: Click [Connect Google Calendar] UI->>AM: POST /connections/initiate<br/>{user_id, agent_id,<br/>tool: "google-calendar",<br/>scopes: ["calendar:read"]} AM->>AM: Build OAuth authorize URL:<br/>client_id = platform's registered<br/>OAuth client with Google<br/>redirect_uri = amp.example.com/<br/>oauth/callback<br/>state = encrypted(user_id, agent_id,<br/>tool, nonce)<br/>code_challenge = PKCE S256 AM-->>UI: {auth_url: "https://accounts.google.com/<br/>o/oauth2/auth?..."} UI->>U: Redirect browser to Google U->>TP: Google's login + consent screen<br/>"Allow AMP Platform to<br/>read your calendar?" Note over U,TP: This is GOOGLE's consent screen.<br/>Tool authorization — the tool itself<br/>requires user consent. U->>TP: Click [Allow] TP-->>AM: Redirect to callback<br/>amp.example.com/oauth/callback<br/>?code=AUTH_CODE&state=... AM->>AM: Decrypt state → validate nonce AM->>TP: POST oauth2.googleapis.com/token<br/>{grant_type: authorization_code,<br/>code: AUTH_CODE,<br/>client_id, client_secret,<br/>code_verifier: PKCE verifier} TP-->>AM: {access_token, refresh_token,<br/>expires_in, scope} AM->>SS: Store Google's refresh_token<br/>key: (user-123, google-calendar)<br/>value: encrypted(refresh_token) SS-->>AM: stored ✓ AM-->>UI: {status: "connected"} UI-->>U: "Google Calendar connected ✓" Note over U,A: ═══ PHASE 3: CHAT (OBO Runtime) ═══ U->>IG: "What's on my calendar today?"<br/>(with session JWT) IG->>IG: Validate user JWT ✓ IG->>A: Forward + x-jwt-assertion header Note over A: LLM reasons → call Google Calendar A->>TH: RFC 8693 token exchange<br/>subject_token: user_assertion (JWT)<br/>actor_token: agent_token<br/>scope: "tools:google-calendar" TH->>TH: Validate user JWT (signature ✓)<br/>Validate agent has scope ✓<br/>User has entitlement ✓<br/>scope = intersection TH-->>A: OBO token<br/>{sub: "user-123",<br/>act: {sub: "calendar-agent"},<br/>scope: "tools:google-calendar"} A->>TG: GET /tools/google-calendar/v1/events<br/>Authorization: Bearer <OBO_token> TG->>TG: Validate OBO locally (cached JWKS)<br/>✓ Signature, expiry, audience<br/>✓ scope "tools:google-calendar"<br/>Extract user_id + agent_id TG->>SS: GET stored credential<br/>key: (user-123, google-calendar) SS-->>TG: {refresh_token: "goog_rt_xxx"} TG->>TP: POST oauth2.googleapis.com/token<br/>(refresh grant) TP-->>TG: {access_token: "goog_at_fresh"} TG->>T: GET googleapis.com/calendar/v3/<br/>calendars/primary/events<br/>Authorization: Bearer goog_at_fresh T-->>TG: calendar events TG-->>A: response A-->>IG: response IG-->>U: "Today you have:<br/>• 9am Team standup<br/>• 2pm Design review" Note over U: ═══ USER LOGOUT ═══Key points:
subject_tokenin OBO exchange — full chain of trustinvalid_grant→ returns 401 → degrades to Case 8 (inline consent)Case 7: OBO + Platform Consent — Pre-auth (M2M Consent)
Complete user session. The tool does NOT require per-user OAuth — it works fine with M2M credentials. But the platform policy requires user consent before the agent can act on their behalf. User grants consent in our UI (no external OAuth dance), then chats with OBO-authenticated tool calls.
sequenceDiagram participant U as End User<br/>(Browser) participant UI as Agent App UI participant AM as Agent Manager participant IG as Ingress Gateway participant A as Agent App<br/>(LLM) participant TH as Thunder (IDP) participant TG as Tool Gateway participant SS as Secret Store participant T as Tool API<br/>(e.g., Internal<br/>HR System) Note over U,UI: ═══ PHASE 1: USER LOGIN ═══ U->>UI: Open agent app in browser UI->>TH: Redirect to Thunder login TH-->>U: Login page U->>TH: Authenticate TH-->>UI: Authorization code → exchange → User JWT Note over U,UI: User is now logged in. Note over U,UI: ═══ PHASE 2: PLATFORM CONSENT ═══<br/>═══ (Developer/admin policy requires it) ═══ Note over U,UI: Agent app shows consent prompt.<br/>Tool supports M2M access — no per-user<br/>OAuth needed. But platform policy<br/>requires user approval. U->>UI: Click [Authorize Agent]<br/>for HR System UI->>AM: POST /connections/consent<br/>{user_id: "user-123",<br/>agent_id: "hr-agent",<br/>tool: "hr-system",<br/>consent_type: "m2m_delegation",<br/>scopes: ["employee:read"]} AM->>AM: Validate:<br/>✓ Agent has tools:hr-system in Thunder<br/>✓ User has hr-system entitlement<br/>✓ Requested scopes within allowed set AM->>AM: Record consent grant:<br/>(user-123, hr-agent, hr-system)<br/>consent_type: m2m_delegation<br/>scopes: [employee:read]<br/>granted_at: now<br/>expires_at: 90 days (policy) AM-->>UI: {status: "authorized",<br/>consent_type: "m2m_delegation"} UI-->>U: "HR System authorized ✓<br/>Agent will use platform<br/>credentials on your behalf" Note over U,UI: No redirect. No OAuth dance with<br/>tool provider. No tokens stored.<br/>Just a consent record. Note over U,A: ═══ PHASE 3: CHAT (OBO Runtime) ═══ U->>IG: "What's my PTO balance?"<br/>(with session JWT) IG->>IG: Validate user JWT ✓ IG->>A: Forward + x-jwt-assertion Note over A: LLM reasons → call HR system A->>TH: RFC 8693 token exchange<br/>subject_token: user_assertion (JWT)<br/>actor_token: agent_token<br/>scope: "tools:hr-system" TH-->>A: OBO token<br/>{sub: "user-123",<br/>act: {sub: "hr-agent"},<br/>scope: "tools:hr-system"} A->>TG: GET /tools/hr-system/v1/pto?user=me<br/>Authorization: Bearer <OBO_token> TG->>TG: Validate OBO locally ✓<br/>scope "tools:hr-system" ✓<br/>Extract user_id + agent_id TG->>AM: Check consent:<br/>(user-123, hr-agent, hr-system)? AM-->>TG: ✓ Consent valid<br/>type=m2m_delegation<br/>scopes=[employee:read] Note over TG: M2M consent path:<br/>Use agent's own M2M credential.<br/>Pass user_id for data filtering. TG->>SS: GET M2M credential<br/>for (hr-agent, hr-system) SS-->>TG: {client_id: "platform_hr_client",<br/>client_secret: "xxx"} TG->>TG: Get M2M access token<br/>(client_credentials with<br/>platform_hr_client) TG->>T: GET hr-api.internal/v1/pto<br/>Authorization: Bearer <M2M_token><br/>X-On-Behalf-Of: user-123 T-->>TG: PTO balance data TG-->>A: response A-->>IG: response IG-->>U: "You have 12 PTO days remaining" Note over U: ═══ USER LOGOUT ═══Key differences from Case 6 (tool authorization):
Case 8: OBO + Inline 401 → Tool Authorization
User has NOT pre-authorized the tool. During chat, the agent attempts to call it, gets a 401, and presents an auth link. The credential flow happens entirely in the browser, bypassing the Agent/LLM. The LLM never sees tokens or secrets — only the auth_url metadata.
This can be triggered for either reason — tool authorization (Google requires OAuth) or platform consent (policy requires approval). The diagram shows tool authorization (OAuth dance).
sequenceDiagram participant U as End User<br/>(Browser) participant IG as Ingress Gateway participant A as Agent App<br/>(LLM) participant TH as Thunder (IDP) participant TG as Tool Gateway participant SS as Secret Store participant AM as Agent Manager participant TP as Tool Provider<br/>(e.g., Google OAuth) participant T as Google Calendar<br/>API Note over U,IG: ═══ PHASE 1: USER LOGIN ═══ U->>IG: Open agent app Note over U,TH: User authenticates via Thunder<br/>→ Session JWT established Note over U,A: ═══ PHASE 2: CHAT ATTEMPT (fails) ═══ U->>IG: "What's on my calendar today?"<br/>(with session JWT) IG->>A: Forward + x-jwt-assertion Note over A: LLM reasons → call Google Calendar A->>TH: RFC 8693 token exchange<br/>subject_token: user JWT<br/>actor_token: agent_token<br/>scope: "tools:google-calendar" TH-->>A: OBO token {sub: "user-123",<br/>act: {sub: "calendar-agent"},<br/>scope: "tools:google-calendar"} A->>TG: GET /tools/google-calendar/v1/events<br/>Authorization: Bearer <OBO_token> TG->>TG: Validate OBO ✓<br/>scope "tools:google-calendar" ✓ TG->>SS: Lookup (user-123, google-calendar) SS-->>TG: NOT FOUND<br/>(no tool authorization) TG-->>A: HTTP 401<br/>{error: "auth_required",<br/>auth_url: "https://amp.example.com/<br/>connect/google-calendar<br/>?agent=calendar-agent<br/>&user=user-123&nonce=abc123",<br/>tool_name: "Google Calendar",<br/>required_scopes: ["calendar:read"]} Note over A: ╔═══════════════════════════════╗<br/>║ SECURITY BOUNDARY ║<br/>║ ║<br/>║ LLM sees ONLY: ║<br/>║ • auth_url ║<br/>║ • tool_name ║<br/>║ • required_scopes ║<br/>║ • error message ║<br/>║ ║<br/>║ LLM NEVER sees: ║<br/>║ • tokens / secrets ║<br/>║ • refresh_tokens ║<br/>║ • client credentials ║<br/>║ • auth codes ║<br/>╚═══════════════════════════════╝ A-->>IG: response IG-->>U: "I need access to your Google<br/>Calendar. Please connect:<br/>[Connect Google Calendar]" Note over U,TP: ═══ PHASE 3: CREDENTIAL FLOW ═══<br/>═══ (BYPASSES LLM ENTIRELY) ═══ rect rgb(230, 245, 230) U->>AM: Click link → new browser tab<br/>https://amp.example.com/<br/>connect/google-calendar<br/>?nonce=abc123 AM->>AM: Validate nonce ✓<br/>Build OAuth URL for Google's<br/>authorization endpoint AM-->>U: Redirect to Google U->>TP: Google's consent screen<br/>"Allow AMP to read your calendar?" U->>TP: Click [Allow] TP-->>AM: Callback with auth_code AM->>TP: Exchange code → tokens TP-->>AM: {access_token, refresh_token} AM->>SS: Store refresh_token<br/>key: (user-123, google-calendar) SS-->>AM: stored ✓ AM-->>U: "Google Calendar connected! ✓<br/>You can close this tab." end Note over U,TP: Tokens flowed through:<br/>Browser → Agent Manager → Google<br/>→ Secret Store. NEVER through Agent/LLM. Note over U,A: ═══ PHASE 4: CHAT RETRY (succeeds) ═══ U->>IG: "OK, I connected it. Try again?" IG->>A: Forward + x-jwt-assertion A->>TG: GET /tools/google-calendar/v1/events<br/>Authorization: Bearer <OBO_token><br/>(reuse cached OBO, still valid) TG->>TG: Validate OBO ✓ TG->>SS: Lookup (user-123, google-calendar) SS-->>TG: {refresh_token: "goog_rt_xxx"}<br/>✓ Now exists! TG->>TP: Refresh → fresh access_token TP-->>TG: {access_token: "goog_at_fresh"} TG->>T: GET googleapis.com/calendar/v3/<br/>calendars/primary/events<br/>Auth: Bearer goog_at_fresh T-->>TG: calendar events TG-->>A: response A-->>IG: response IG-->>U: "Today you have:<br/>• 9am Team standup<br/>• 2pm Design review"Critical security property: The
rectblock (green-highlighted) shows the credential flow that completely bypasses the Agent/LLM. Auth codes, tokens, and refresh tokens flow throughBrowser → Agent Manager → Tool Provider → Secret Store. The LLM only ever sees theauth_urlstring and the eventual 200 success response.Inline platform consent variant: If the 401 is due to missing platform consent (not tool authorization), Phase 3 would show our consent UI instead of the OAuth dance — same 401 trigger, but user approves in our UI and a consent record is stored instead of tokens.
Future Cases to Consider
tools/listby scope claims and validatestools/callagainst scope. Same auth model, different transport framing.User Token Acquisition — OAuth Intermediary
When a tool requires user-delegated access (e.g., user's Google Calendar), the user must first connect their account. The platform acts as an OAuth intermediary, the same pattern used by Auth0 Token Vault, Composio, and Nango.
Pre-authorization flow (primary, best UX):
User connects their tool accounts through the agent's own UI before using the agent. This is the dominant industry pattern [4][5].
How user tokens get into the secret store: See Approach Diagrams — Case 6 (tool authorization pre-auth), Case 7 (platform consent pre-auth), Case 8 (inline 401 consent).
Key details:
/oauth/callback). Agent Manager is the OAuth intermediary.refresh_tokenin secret store, keyed by(user_id, tool_provider). Access tokens are short-lived and regenerated at runtime.(user_id, tool_provider)— if a user connects Gmail, any agent withtools:gcalscope can use it.Consent Handling
LLM-Inline Fallback (for tools not pre-authorized)
If a tool wasn't pre-authorized and the agent tries to use it, gateway returns a 401 with an
auth_url. The LLM naturally presents it in chat [8][9]. The credential exchange (OAuth dance) happens entirely in the browser, bypassing the Agent/LLM — the LLM only ever sees theauth_urlmetadata, never tokens or secrets. See Approach Diagrams — Case 8 (inline 401 → tool authorization) for the detailed flow.CIBA Step-Up (for high-risk actions)
Even with pre-authorization, some actions need explicit approval (e.g., payments, deleting data). Gateway evaluates consent policy and returns HTTP 202 + triggers CIBA via Agent Manager → Thunder → push notification to user's device. User approves/denies. Agent retries after approval. Per-action consent via CIBA [12][13].
MCP Support
For MCP-native agents, the same gateway can expose an MCP server interface. The MCP spec (2025-03-26) mandates OAuth 2.1 with
Authorization: Beareron every HTTP request [42]. This maps directly to our OBO token model:tools/callrequestOur approach: We follow the shared server + user token pattern. Our gateway is a shared MCP server that validates the OBO token on every request, extracts both agent and user identity from claims (
act+sub), and resolves credentials accordingly.Tool-level authorization at gateway:
tools/listtools/callscopeclaim againstparams.nameparams.argumentsagainst claims (Phase 4)MCP gateway is additive: same backend, different transport.
Developer Experience
Agent Developer Code
What each per-tool OBO token contains:
{ "sub": "user-123", "act": { "sub": "travel-assistant" }, "iss": "https://thunder.example.com", "aud": "tool-gateway", "scope": "tools:twilio", "exp": 1711382400 }Comparison to industry:
@tool_call(scope="tools:twilio")decorator@requires_access_token(provider, scopes)decoratoruser_id=on every SDK callAuthorizationheader to sidecarOur pattern directly matches AWS AgentCore and Auth0: per-tool decorator handles token exchange lazily, one narrow-scope OBO per tool per session.
Console Wireframes
Create Agent:
Register Tools (Agent Detail → Tools Tab):
Deploy Flow
sequenceDiagram participant D as Developer participant C as AMP Console participant AM as Agent Manager participant TH as Thunder participant GW as API Gateway participant K as K8s D->>C: deploy agent C->>AM: POST /agents AM->>TH: create NHI "travel-assistant-prod"<br/>user nature + app nature (client_credentials)<br/>scopes: tools:twilio, tools:gcal TH-->>AM: agent_id + client_id + secret AM->>GW: store tool credentials (API keys, OAuth secrets) AM->>GW: register tool proxy routes<br/>/tools/twilio/*, /tools/gcal/* GW-->>AM: routes registered AM->>K: create deployment with env vars:<br/>AGENT_CLIENT_ID, AGENT_CLIENT_SECRET,<br/>TOOL_GATEWAY_URL, THUNDER_TOKEN_URL K-->>AM: deployment ready AM-->>C: agent deployed C-->>D: "Deployed"Out of Scope
actclaims or Okta ID-JAG). Deferred to Phase 4+.Alternatives Considered
user_id. No OAuth logic in agent code.Open Questions
urn:ietf:params:oauth:grant-type:token-exchangetoday? What about CIBA (urn:openid:params:grant-type:ciba)?/tools/twilio/*→api.twilio.com/*) or header-based?x-jwt-assertion? If not, what header carries user claims? Need to align with existing gateway behavior.actclaim supports nested actors.Milestones
sub+actclaims, validated via cached JWKS). Credential translation. Audit logging at gateway level.Beta Was this translation helpful? Give feedback.
All reactions