What this guide covers, and where the credentials guide stops
Our guide to least-privilege credentials for AI agents answers one question: when an agent acts as itself, what credential should it hold, where should it live, and how small can the permission set be? Read it first — everything here assumes it.
This guide answers a different one. When agent A spawns sub-agent B to act on a named human's behalf, how does B prove who it is, whose authority it carries, and how far that authority extends? That is not a secrets problem, it is a chain problem — and you can have immaculate credential hygiene and still fail an enterprise security review, because the reviewer's question is not "where is the key" but "who asked for this, which component executed it, and how do you know?"
- "The agent has the user's token" is the wrong model. It flattens four distinct identities into one and destroys attribution exactly when you need it.
- RFC 8693 token exchange is the primitive. A parent exchanges its token for a narrower one for the sub-agent, and the
actclaim keeps the chain intact. - Delegation and impersonation differ, and the RFC says so. Delegation preserves the actor; impersonation erases it. Audit needs delegation.
- DPoP (RFC 9449) makes a leaked token useless to whoever picked it up, by binding it to a key the holder must prove possession of on every request.
- Workload identity federation removes the static secret — the compute platform attests the workload and short-lived credentials follow.
- The RFCs are settled; the MCP bindings are in flight. Build on the former, track the latter.
Why long-lived keys break the moment an agent spawns an agent
Start with the shape almost every team ships first. A user authenticates, the application obtains a token on their behalf, the agent runtime is handed that token, and it calls tools with it — passing the same token down whenever it spawns a specialist sub-agent. Works in the demo, wrong in four ways.
It is a confused deputy waiting to happen. A privileged component gets tricked into misusing its authority for a less privileged caller — and agent stacks are a rich environment for it, because the instructions that steer the deputy arrive as data. A retrieval sub-agent reads a document; the document tells it to call the payments tool; the sub-agent is holding a token that permits exactly that, because it inherited the parent's full authority. Nothing was compromised in the traditional sense: the deputy was confused, and happened to be carrying a key that opened every door. Our guide on designing agents that fail safe covers containment; the point here is to make the sub-agent structurally incapable of it.
Revocation becomes all-or-nothing, and expiry is wrong in both directions. The only lever is revoking the human's token, which stops every other agent and session using it, so teams learn that pulling it costs more than the incident. Meanwhile a sub-agent spawned to summarise one PDF holds credentials still valid next quarter, sitting in a memory dump, a log line or a serialised queue record.
And audit collapses. This is the failure that ends procurement conversations. Every call reaches the resource server with the same sub, client identifier and scopes, so the retrieval sub-agent's read, the parent's read, the code sub-agent's write and the human's own click are indistinguishable rows. When an RBI inspection at an Indian bank's captive centre, or an FCA-driven review at a UK payments firm, asks "show me every action this agent took on this account, and which component initiated each one", the honest answer is that the data was never captured.
Passing the user's access token down to sub-agents unchanged, on the reasoning that "they are all part of the same application anyway". They are not. The moment one of them reads untrusted input it stops being part of your application in any meaningful security sense.
The four identity layers, and what breaks when you collapse them
Most agent authorisation failures reduce to one root cause: four distinct identities squashed into a single bearer token. Each answers a question the others cannot.
| Layer | What it is | The question it answers | What breaks if you collapse it |
|---|---|---|---|
| Human principal | The person whose data and authority are in play, as sub |
On whose behalf, and who consented? | Consent is unattributable; DPDP and UK GDPR subject requests cannot be answered |
| Application / client | The registered OAuth client, as client_id |
Which product asked, under which registration? | No way to revoke one integration without revoking the user |
| Workload | The running process or container, with an attested identity | Which deployment, environment and region is executing this? | Staging cannot be told from production; a compromised node cannot be isolated |
| Agent instance | The individual reasoning loop, task-scoped | Which instance, spawned by whom, for which task? | Attribution dies; blast radius cannot be bounded; one sub-agent cannot be stopped alone |
The fourth layer is the one teams skip and the one enterprise buyers ask about — and it is cheap to add once the third exists, because a sub-agent identity is usually a task-scoped credential minted from the workload's own. The rule: no layer should hold a credential belonging to a layer above it.
Before writing code, draw your agent topology and label every arrow with the four layers it carries. Arrows where the same token appears at both ends are your delegation gaps. The exercise takes an afternoon and usually finds three or four places where a sub-agent holds authority nobody meant to give it.
RFC 8693 token exchange, and the act chain it preserves
OAuth 2.0 Token Exchange, published as RFC 8693 in January 2020 on the Standards Track, is the primitive that does the work. It is stable, widely implemented, and long predates the current interest in agents — which is why it is a safe foundation.
The parent presents the token it holds and asks the authorization server for a different one: narrower in scope, addressed to a specific resource, shorter-lived. Three parameters are required — grant_type, subject_token and subject_token_type. The subject token represents the party on whose behalf the request is made; here, the human. The optional parameters are where the narrowing happens.
# Parent agent asks the authorization server for a sub-agent token.
# Only grant_type, subject_token and subject_token_type are REQUIRED.
POST /as/token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9.PARENT_TOKEN
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&actor_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.SUBAGENT_WORKLOAD_SVID
&actor_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt
&resource=https%3A%2F%2Fledger.internal.example.com%2Fapi
&scope=ledger.read
&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
# Response (application/json): access_token, issued_token_type and
# token_type are REQUIRED; expires_in is RECOMMENDED; scope is returned
# when it differs from what was requested.
The roles are easy to swap by accident. subject_token is the party being acted for; actor_token is the party doing the acting — the sub-agent's own identity — and actor_token_type is required whenever actor_token is present and must not be sent otherwise. resource and audience name where the token may be used, scope asks for a smaller permission set, and requested_token_type says what format you want back. Token type identifiers are URNs of the form urn:ietf:params:oauth:token-type:access_token, with siblings for refresh_token, id_token, jwt, saml1 and saml2.
The act claim, and why nesting is the point
What comes back still names the human as sub, but now carries an act claim naming the actor. When a sub-agent spawns a sub-agent of its own, the exchange repeats and the claims nest. The outermost act names the current actor; nested act claims name prior actors.
# Decoded payload of the token issued to the sub-agent.
# sub is still the human. act names who is actually calling.
{
"iss": "https://as.example.com",
"aud": "https://ledger.internal.example.com/api",
"sub": "ananya.r@bank.example.in",
"iat": 1787740800,
"exp": 1787741100,
"scope": "ledger.read",
"client_id": "recon-console",
"act": {
"sub": "spiffe://bank.example.in/agents/ledger-reader",
"act": {
"sub": "spiffe://bank.example.in/agents/recon-parent"
}
},
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}
# Read it outward-in: the ledger-reader sub-agent is calling now;
# it was delegated to by recon-parent; both act for Ananya.
# The scope claim is a space-separated string, per RFC 8693.
Two things follow. A resource server that understands act can express policy the flat model cannot: permit a read for any actor in the chain, but require the outermost actor to be a specific workload before permitting a write. And the chain is now in the token, so it can be logged verbatim without trusting the agent runtime to report it honestly.
RFC 8693 also defines may_act, which runs the other way: it states that one party is authorised to become the actor and act on behalf of another. Placed on the human's token, it pre-authorises the exchange — so the identity provider, not the agent runtime, holds the policy about which workloads may ever act for which users.
Not every authorization server implements the full RFC 8693 surface. subject_token and downscoping are common; actor_token, the nested act claim and may_act are patchier. Issue an exchange, decode the result, and confirm act is present and nests as expected. A provider that silently drops the actor has given you impersonation while you believed you had delegation.
Delegation or impersonation: choose deliberately
RFC 8693 is explicit that these are two different things, and it is the most consequential decision in this area. Under impersonation, in the specification's words, "when principal A impersonates principal B, A is given all the rights that B has within some defined rights context and is indistinguishable from B in that context". Under delegation, "principal A still has its own identity separate from B, and it is explicitly understood that while B may have delegated some of its rights to A, any actions taken are being taken by A representing B". One erases the actor; the other keeps it.
| Delegation | Impersonation | |
|---|---|---|
| Actor chain | Preserved — act names the actor, nesting per hop |
Erased — only the human's sub survives |
| Audit consequence | A reviewer can name the component that made a call | The log says the human did it; nothing says otherwise |
| Revocation | One actor cut off without touching the human | All or nothing — revoke the human or accept the risk |
| Policy | Rules can depend on the actor, the human, or both | Rules can depend only on the human |
| When to use it | Default for every sub-agent and automated hop | A legacy resource server that cannot read act, with a documented retirement date |
Default to delegation everywhere, and treat any impersonation path as an exception with a named owner and an expiry date. If you cannot answer "which component made this call" from the token alone, you have impersonation whether or not you chose it.
DPoP: making a leaked token useless to whoever picked it up
Narrowing authority is necessary but not sufficient: a narrow bearer token is still a bearer token, and agent systems leak tokens unusually well — into trace spans, structured logs, error reports, serialised task queues and prompt histories. RFC 9449, "OAuth 2.0 Demonstrating Proof of Possession (DPoP)", September 2023, Standards Track, addresses exactly this. The client generates a key pair and sends a signed DPoP proof JWT with every request; the access token is bound to the public key, so a token without the matching private key cannot be used.
# The DPoP proof JWT -- one per request, sent in the DPoP header.
# RFC 9449 s4.2: the JOSE header MUST contain typ, alg and jwk.
# --- JOSE header ---
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA"
}
}
# jwk carries the PUBLIC key only. It MUST NOT contain a private key.
# alg MUST NOT be "none" or a symmetric algorithm.
# --- payload ---
{
"jti": "e1j3V_bKic8-LAEB",
"htm": "GET",
"htu": "https://ledger.internal.example.com/api/entries",
"iat": 1787740812,
"ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo",
"nonce": "eyJ7S_zG.eyJH0-Z.HX4w-7v"
}
# jti unique per proof, for replay detection
# htm the HTTP method of this request
# htu the target URI, WITHOUT query and fragment parts
# iat creation time of the proof
# ath base64url SHA-256 hash of the ASCII access token value,
# included whenever an access token is presented
# nonce a recent value supplied via the DPoP-Nonce HTTP header
The binding lives in the access token as a confirmation claim. RFC 9449 specifies that the value of jkt is the base64url encoding of the JWK SHA-256 Thumbprint of the DPoP public key — the same cnf.jkt you saw in the delegated token above. The resource server computes the thumbprint of the key in the proof's jwk header, compares it with cnf.jkt, verifies the signature, checks htm and htu against the request received, and checks ath against the token presented. All five must agree.
# A DPoP-bound request. The Authorization scheme is DPoP, not Bearer,
# and the token response carries token_type: DPoP.
GET /api/entries?period=2026-08 HTTP/1.1
Host: ledger.internal.example.com
Authorization: DPoP Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...
# If the server requires a fresh nonce it answers with the DPoP-Nonce
# header and the error code use_dpop_nonce -- HTTP 400 at the
# authorization server, or a 401 with WWW-Authenticate at the resource
# server. The client retries with the supplied nonce. Handle this on the
# first request rather than treating it as a failure.
Note what DPoP does not do. It does not narrow authority — a DPoP-bound token with excessive scope is still excessive — and it does not help if the attacker has compromised the process and holds the private key. It is a replay control, composing with narrow scopes and short lifetimes rather than substituting for them.
"We turned on DPoP for our internal agent mesh mostly as a compliance exercise, and it paid for itself in week three. An observability agent was writing full request headers into a trace store half the company could read. Under bearer tokens that would have been a disclosure incident with a notification clock running. Under DPoP it was a rotation, an apology and a ticket."
— Anita, Verified Builder · London, United KingdomWorkload identity federation: removing the static secret
Everything above still assumes the parent holds something to authenticate with at the token endpoint. Workload identity federation removes even that. The platform issues the running workload a short-lived, signed identity document with a platform-controlled issuer, audience and subject; the workload presents it to an authorization server or cloud STS configured to trust that issuer; short-lived credentials come back. No static secret was created, stored, rotated or leaked, because none exists.
- Kubernetes. A projected service account token, audience-bound and short-lived, obtained through the pod's own service account and verifiable via the cluster's OIDC discovery endpoint.
- Cloud workload identity. Each major provider accepts an external OIDC token and returns short-lived cloud credentials, mapping external subject to internal principal. The same machinery underpins keyless CI deployments — which is why many teams already run it and have simply not connected it to their agent runtime.
- SPIFFE and SPIRE. A platform-neutral scheme where every workload receives a SPIFFE ID of the form
spiffe://trust-domain/path, for examplespiffe://acme.com/billing/payments. The identity is carried in an SVID, a cryptographically verifiable document issued as either an X.509 certificate or a JWT, fetched through a local Workload API that requires the workload to hold no prior credential at all.
That last property is what makes SPIFFE attractive for agents: it solves the bottom turtle without a secret. And it connects straight back to the exchange — a SPIFFE JWT-SVID is exactly what belongs in actor_token. The workload proves what it is; the human's token proves on whose behalf; the exchange produces a credential carrying both.
Every article here is written by a Verified Builder. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Scope design for sub-agents: narrow, addressed, time-boxed
A downscoped token is only as useful as the scopes available to downscope to, and most APIs offer scopes designed for applications — read, write, admin — far too coarse to express what a sub-agent should be allowed to do for the next ninety seconds. Three levers make scopes meaningful again.
Address the token, do not just narrow it. RFC 8707, "Resource Indicators for OAuth 2.0", February 2020, Standards Track, defines the resource parameter. Its value must be an absolute URI and must not include a fragment component; query components should generally be avoided; multiple resource parameters may be sent for a token genuinely intended for several resources. The authorization server should then audience-restrict the issued token to those resources, typically via aud. A token minted for the ledger API is therefore rejected outright by the payments API — a sub-agent tricked into calling the wrong service fails at the door, not at your policy engine.
Time-box aggressively. A sub-agent token's lifetime should be the expected task duration plus a small margin — seconds or a few minutes, not hours. Combined with DPoP, a token found in a log is both unusable and already expired.
Make high-risk authority one-shot. For irreversible actions — a payment, a deletion, an outbound message to a customer — issue a token whose scope names the individual operation and whose jti the resource server records and refuses to accept twice.
When the agent must stop and ask the human
Delegated authority is not a substitute for consent, and the boundary should be a design decision rather than an accident of what the token happened to permit. Model it as a distinct authorisation step: the sub-agent's token does not carry the scope, the attempt fails, and the failure triggers a fresh consent flow in which the human sees the specific action requested. Three triggers are worth hard-coding — crossing a value threshold, acting on a data class the original consent did not cover, and any action that cannot be undone. Our guide to sampling and elicitation in MCP servers covers how to surface the request through the client rather than inventing a side channel.
| Standard | What it solves | Status, as of August 2026 |
|---|---|---|
| RFC 8693 — Token Exchange | Narrowing a token; delegation semantics via act; pre-authorised actors via may_act |
Standards Track, Jan 2020. Settled and widely implemented, though actor_token and nested act support varies |
| RFC 9449 — DPoP | Sender-constrained tokens: a leak is useless without the signing key | Standards Track, Sep 2023. Settled; adoption growing. A priority deliverable on the MCP roadmap of 22 August 2026 |
| RFC 8707 — Resource Indicators | Addressing a token to a named resource so it is rejected elsewhere | Standards Track, Feb 2020. Settled and simple — the cheapest item here |
| OIDC federation / workload identity | An identity for a workload with no static secret, via platform attestation | Mature across Kubernetes and the major clouds; SPIFFE and SPIRE are the neutral form. A pattern, not one RFC |
| ID-JAG — Identity Assertion JWT Authorization Grant | An enterprise IdP minting a grant for a third-party resource, keeping policy central | IETF draft draft-ietf-oauth-identity-assertion-authz-grant-04, May 2026. In flight. Requested via requested_token_type of urn:ietf:params:oauth:token-type:id-jag |
A worked example, traced from human to sub-agent to tool
The platform team at an Indian private bank's captive centre runs a month-end reconciliation agent. Ananya, a finance operations analyst, asks it to reconcile the August ledger against the settlement file and raise exceptions. Reading the ledger and creating tickets are two different authorities, belonging to two different sub-agents.
Hop 0 — the human. Ananya signs in through the bank's IdP and consents to the reconciliation console acting on her behalf. The console receives a short-lived, DPoP-bound token with her sub, a client_id of recon-console, and scopes covering ledger reads and exception creation. Note what has not happened: no agent has touched this token, and none will.
Hop 1 — the parent agent. The parent runs as an attested workload, spiffe://bank.example.in/agents/recon-parent. It exchanges Ananya's token: subject_token is her token, actor_token is its own JWT-SVID, scope is trimmed to the two operations needed, resource names the ledger and ticketing APIs. Back comes a token with her sub, an act naming the parent, and a five-minute lifetime.
Hop 2 — the reading sub-agent. The parent spawns a ledger reader, attested as spiffe://bank.example.in/agents/ledger-reader, and exchanges again: scope=ledger.read, resource naming only the ledger API, ninety-second lifetime. The issued token carries Ananya's sub, an act naming the reader, a nested act naming the parent, and a cnf.jkt bound to the reader's key. It cannot create a ticket — not because policy forbids it, but because the scope was never granted and the audience never set. When the reader ingests a settlement file containing an instruction to make a payment, the attempt fails at the ledger API's door.
Hop 3 — the writing sub-agent. A separate exception filer gets its own exchange: scope=tickets.create, resource naming only the ticketing API. It never holds read authority over the ledger.
Hop 4 — the escalation. One exception exceeds the auto-write-off threshold. The filer's token does not carry that scope; the call fails cleanly; the parent raises an elicitation through the console; Ananya sees the amount and account and approves. That mints fresh one-shot authority for that single operation, appearing in the audit trail as a separate consent event rather than a continuation of the original delegation.
What the audit trail must capture
The record below is what a reviewer at an NHS trust, a UK payments firm or an RBI-supervised bank actually wants. Every field derives from the token presented at the resource server, not from the agent runtime — the property that makes it evidence rather than telemetry.
{
"event_id": "01J9F2K7Q3ZC8V4M6N0PXR5TDB",
"ts": "2026-08-26T09:41:07.442Z",
"principal": {
"sub": "ananya.r@bank.example.in",
"client_id": "recon-console",
"auth_time": "2026-08-26T09:12:44Z",
"consent_id": "cns_7f21ab9e"
},
"delegation_chain": [
"spiffe://bank.example.in/agents/ledger-reader",
"spiffe://bank.example.in/agents/recon-parent"
],
"chain_source": "act_claim",
"workload": {
"spiffe_id": "spiffe://bank.example.in/agents/ledger-reader",
"cluster": "prod-mum-1",
"region": "ap-south-1",
"image_digest": "sha256:9c4b...e07a"
},
"task": {
"root_task_id": "tsk_aug_recon_0826",
"agent_instance_id": "ai_5b3d9c2f",
"parent_instance_id": "ai_1a77e004"
},
"authorization": {
"scope": "ledger.read",
"aud": "https://ledger.internal.example.com/api",
"token_jti": "3f9c1e77-2b04-4a51-9df6-8c2b71a0e4d3",
"cnf_jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I",
"exp": "2026-08-26T09:42:37Z",
"one_shot": false
},
"action": {
"method": "GET",
"target": "https://ledger.internal.example.com/api/entries",
"params_digest": "sha256:41ac...9b12",
"outcome": "allow",
"status": 200
},
"escalation": null
}
Four fields carry the weight. delegation_chain, read from the act claim, answers "which component did this". principal.consent_id ties the chain to a human consent event rather than a session. authorization.cnf_jkt proves the caller held the bound key. And chain_source distinguishes a cryptographically carried chain from one the runtime asserted about itself. Log params_digest rather than parameters — prove which call was made without copying customer data into a second system that then falls under DPDP and UK GDPR in its own right.
What MCP is proposing, as of August 2026
The Model Context Protocol roadmap last updated on 22 August 2026 makes agent identity one of five priority areas, framing the problem in almost these terms: "MCP authorization assumes a person with a browser at consent time", increasingly the caller is "a cloud workload with its own identity, acting for a user who isn't present, or spawning sub-agents that should get narrower authority than their parent", and "existing MCP servers lean on pasted API keys and long-lived refresh tokens". Our coverage of the roadmap and what it means for agent identity goes through the wider document; what matters here is the two named deliverables.
The first is DPoP: an Agent Identity Working Group, described as forming during this roadmap period, is to "finalize the specification for Demonstrating Proof of Possession (DPoP) and focus on getting widespread adoption". The second is agent identity and delegation: the same group wants "an opinionated way for MCP servers to be reached by agents through their own identity or a user-delegated identity", with work focused on Workload Identity Federation under SEP-1933, the Identity Assertion JWT Authorization Grant used by Enterprise-Managed Authorization, and RFC 8693 token exchange, coordinated with the IETF OAuth and WIMSE working groups. Human-presence attestation — distinguishing interactive clients from headless agents — is noted as under discussion.
Two cautions. The roadmap explicitly says it "reflects current thinking rather than firm commitments" and that items may shift or be deferred; none of it is shipped specification as of August 2026. And, worth stating because it circulates widely: an act_as-style claim appears in third-party commentary about the roadmap, not in the roadmap text. MCP does not specify such a claim. The claim the standards define is act, in RFC 8693. Build on the RFC.
The related Enterprise-Managed Authorization extension is further along and shows the likely shape: an enterprise IdP mints an ID-JAG for a specific MCP server after evaluating organisational policy, and the client exchanges that for an access token from the MCP authorization server, with no per-server consent prompt. Centralised policy, centralised revocation — what a UK financial services security team or an Indian GCC's IT function will insist on before agents touch production. If you run an MCP server, the groundwork is the same as migrating to the stateless spec: clean separation between transport, session and authority.
The migration ladder, and the pitfalls on the way up
Nobody moves from pasted API keys to federated, DPoP-bound, delegated short-lived credentials in one sprint. Each rung below is independently shippable — you can stop at any point and still be better off.
| Rung | What you do | Effort | What it unlocks |
|---|---|---|---|
| 1. Inventory | List every credential every agent holds, its lifetime, scope and readers | Days | An honest baseline, and usually credentials nobody knew were in play |
| 2. Distinct clients | Register each agent surface as its own OAuth client | Days | Per-integration revocation; client_id becomes meaningful in logs |
| 3. Shorten and address | Cut lifetimes to task duration; add resource (RFC 8707) |
Days to a week | A leaked token is expired and wrong-audience; cross-service misuse fails at the door |
| 4. Token exchange | Parent exchanges rather than forwards; each sub-agent gets a downscoped token with act |
Weeks | Real least privilege per sub-agent, and an attributable chain in every token |
| 5. Chain-aware audit | Log the act chain, consent id, workload identity and token jti |
Days, after rung 4 | The evidence buyers and regulators ask for; incident scoping in minutes |
| 6. DPoP | Sender-constrain tokens; verify cnf.jkt; handle the nonce round trip |
Weeks | Token leakage becomes a rotation rather than an incident |
| 7. Workload federation | Replace the last static secrets with platform attestation; feed the SVID in as actor_token |
Weeks to a quarter | No long-lived secret exists to steal, rotate or leak |
Common pitfalls
- Exchanging once and caching the result. A token minted for one task and reused across tasks is a long-lived credential again, and its
actchain now attributes unrelated work to the wrong root task. Cache by task, never by process. - Downscoping to scopes that are not narrower. If your API offers only
readandwrite, a downscoped token is barely downscoped. Adding operation-level scopes to your own APIs sets the ceiling for everything above it. - Assuming your authorization server preserves
act. Several accept the request, ignoreactor_tokenand return a plain downscoped token. Decode and assert on the issued token in a test. - Treating DPoP nonces as an error. The
use_dpop_nonceresponse is a normal part of the handshake, not an outage. - Logging the chain from the runtime rather than the token. A chain the agent reports about itself is worth exactly as much as the agent's integrity.
- Extending delegated authority to unvetted third-party servers. A downscoped token handed to an MCP server of unknown provenance is still a token. All of this assumes you have done the work in our guide to vetting MCP servers and agent skills first.
The through-line survives whatever the MCP working groups eventually publish. Authority in an agent system is not a possession, it is a chain: every hop narrower than the one above it, and attributable to it. The RFCs were finished years before anyone shipped a sub-agent. What is new is the obligation to use them.
Sources
- RFC 8693 — OAuth 2.0 Token Exchange
- RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- RFC 8707 — Resource Indicators for OAuth 2.0
- draft-ietf-oauth-identity-assertion-authz-grant — Identity Assertion JWT Authorization Grant
- Model Context Protocol — Roadmap (last updated 22 August 2026)
- Model Context Protocol — Enterprise-Managed Authorization extension
- SPIFFE — Concepts: SPIFFE ID, SVID and the Workload API