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 act claim 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.

Avoid

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.

Pro tip

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.

Watch out

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
Recommended

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.

From a verified Builder

"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 Kingdom

Workload 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 example spiffe://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 act chain 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 read and write, 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, ignore actor_token and return a plain downscoped token. Decode and assert on the issued token in a test.
  • Treating DPoP nonces as an error. The use_dpop_nonce response 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