API Keys Were Never Going to Survive AI Agents: The AuthN/AuthZ Patterns That Will
Sources
- RFC 8693: OAuth 2.0 Token Exchange (STS, act/may_act claims)
- MCP Authorization specification, revision 2026-07-28
- MCP Enterprise-Managed Authorization extension (Cross App Access profile)
- MCP discussion #804: A Gateway-Based Authorization Model (scope-model limits)
- SPIFFE — SPIRE Concepts (attestation, registration entries, SVIDs)
- Uber Engineering — Our Journey Adopting SPIFFE/SPIRE at Scale
- Indeed Engineering — Secure Workload Identity with SPIRE and OIDC
- Anthropic — Use WIF with SPIFFE (JWT-SVID federation to the Claude API)
- AWS — On-behalf-of token exchange with Bedrock AgentCore Identity
- AWS Security Blog — Propagate user authorization context in AI agents
- Microsoft Learn — Entra Agent ID: authentication protocols in agents
- Okta — Agent SSO GA press release (Cross App Access in core SSO)
- Google Cloud — Agent Identity overview (SPIFFE-based, cert-bound tokens)
- A2A Protocol Specification — Agent Card security schemes
- OAuth.net — Cross App Access / IETF ID-JAG draft
The most dangerous phrase in an agent rollout is "we gave it an API key." A static key grants an agent permanent, un-scoped, un-attributed access to production systems — and agents are the one workload class that combines non-determinism with tool use. Okta's March 2026 press release cites research finding that 88% of organizations report suspected or confirmed AI agent security incidents, while only 22% treat agents as independent, identity-bearing entities. That gap is the entire story: fleets of agents are being deployed with the credential hygiene of a 2013 cron job. The stat comes from Okta's Agent SSO announcement — and the vendor has a commercial reason to make it scary, which is exactly why the number deserves attention: even discounted, it describes an industry giving production access to workloads it cannot name.
This guide maps the patterns that replace static keys for agent authentication and authorization — OAuth client credentials, RFC 8693 token exchange, the MCP authorization spec, SPIFFE/SPIRE workload identity, and A2A security schemes — each with an honest weakness list and a who-should-skip verdict. It assumes you already run agents on Kubernetes or an equivalent fleet platform; if not, start with our sandbox-and-egress guide for agents on Kubernetes and the fleet architecture patterns piece.
Why API Keys Break at Agent Scale
A static API key fails four ways at once when the holder is an autonomous agent:
- No rotation story that survives a fleet. Keys rotate on human cadences — quarterly at best. A fleet of hundreds of agents redeployed daily means either keys live un-rotated in secrets stores or rotation becomes a full-time operational tax. Anthropic's own workload identity federation docs frame the goal bluntly: "There are no static secrets to mint, store in CI, rotate, or leak."
- No attribution. A key shared by an agent, its sandbox, and a developer's local test harness produces audit logs that cannot answer "which actor did this." When an agent takes an unexpected action, the post-mortem needs a chain: which agent, acting for which user, under which delegated authority.
- Unbounded blast radius. The key's permissions are fixed at issue time, not scoped per task. An agent that legitimately needs read access to one CRM record class gets the key's full envelope — and prompt injection just found a permanent credential.
- No delegation semantics. An agent acting "on behalf of" a user through a shared key is indistinguishable from the user themselves. There is no way to grant less authority than the key carries, and no way for the downstream service to see that a machine was in the loop.
The replacement is not a single technology. It is a stack: short-lived, audience-bound OAuth tokens for sessions; token exchange for delegation chains; an attested workload identity underneath the fleet; and per-request policy at the tool boundary. Here is the shape of it:
flowchart TB
subgraph IDP["Identity provider / OAuth authorization server"]
AS["OAuth 2.1 AS - short-lived, audience-bound tokens"]
end
subgraph FLEET["Agent fleet on Kubernetes"]
SPIRE["SPIRE agents - attest pods, deliver SVIDs"]
P["Planner agent"]
E["Executor agent"]
X["Specialist agent"]
end
subgraph TOOLS["Downstream systems"]
MCP["MCP tool server"]
API["Vendor APIs"]
end
U["Human user"] -->|"OIDC login + consent"| AS
AS -->|"auth code + PKCE"| P
P -->|"token exchange - act chain"| E
E -->|"audience-bound token"| MCP
E -->|"A2A security scheme"| X
X -->|"OBO token exchange"| API
SPIRE -.->|"X.509 / JWT-SVID, no secret"| P
SPIRE -.-> E
SPIRE -.-> X
Pattern 1 — OAuth Client Credentials: The Autonomous Baseline
A fully autonomous agent — scheduled triggers, no human in the loop — maps cleanly to the OAuth 2.0 client credentials grant: the agent authenticates as itself, and gets a token that identifies the agent, not any user. Microsoft's Entra Agent ID documentation describes exactly this mode: "Agent identities use client_credentials for app-only autonomous operations, enabling independent functionality without user context," while agents acting for a user use the on-behalf-of flow instead. The spec-relevant point is that client credentials gives you identity and rotation, but not delegation — the token says "agent X is acting," never "agent X is acting for user Y."
The reason this pattern exists in the stack is credential lifecycle: the agent presents a secret (or better, a federated workload credential — see Pattern 4) to the authorization server and receives a short-lived access token. Revocation is a policy action against the agent's registration, not a key hunt across the estate.
Honest weaknesses.
- Scope granularity is whatever the resource server supports. Most vendor APIs expose coarse scopes, so "least privilege" degrades to "least scope the vendor offers."
- No user context flows downstream. Any per-user filtering has to be re-implemented in application code — the exact failure mode AWS documents when it contrasts client-credentials "broad access" with per-user on-behalf-of tokens.
- Client secrets are still static credentials — you have moved the secret from the tool API to the IdP, which is progress, but the agent still needs something to authenticate with. Without an attested identity layer underneath, you have traded one long-lived secret for another.
Who should skip: nobody entirely — client credentials is table stakes for background agents. But if your agent acts on user-visible data (documents, tickets, records), client credentials alone is the wrong pattern even when it works; skip to token exchange. And if your "agent" is really a scheduled script with one API to call, a scoped, rotated key in a managed secret store remains the pragmatic answer — OAuth machinery for a single-caller integration is over-engineering.
Pattern 2 — RFC 8693 Token Exchange: Delegation Chains That Name the Actor
OAuth 2.0 Token Exchange turns an authorization server into a Security Token Service: a client presents a subject_token (the user's token — the mandate) and optionally an actor_token (its own credential — the acting party), and receives a new, scoped token for a downstream audience. RFC 8693 could not describe the agent use case better if it had been written for it — the RFC's own words: "with delegation semantics, principal A still has its own identity separate from B... In a sense, A is an agent for B."
The output is a composite token. The sub claim stays the user; an act claim names the current actor; nested act claims preserve the prior actors as a history trail. This is the audit primitive that API keys cannot produce:
{
"iss": "https://as.example.com",
"sub": "user@example.net",
"aud": "urn:example:cooperation-context",
"scope": "status feed",
"act": {
"sub": "https://agent-executor.example.com",
"act": {
"sub": "https://agent-planner.example.com"
}
}
}
sub user@example.net <- the mandate: who authorized this
act.sub https://agent-executor.example.com <- current actor: who acts NOW
act.act.sub https://agent-planner.example.com <- prior actor: delegation history
RFC 8693 rule: access control uses only the top-level
claims and the CURRENT actor. Prior actors are
informational — an audit trail, not an authorization input.
The may_act claim closes the loop on the other side: it lets a token declare who is authorized to become its actor, so the authorization server can reject an exchange attempt by an unlisted agent before any downstream call happens.
Real deployments. This is now shipped vendor infrastructure, not theory. Amazon Bedrock AgentCore Identity supports on-behalf-of (OBO) token exchange natively — generally available in 14 AWS Regions as of April 2026 — with the agent never touching client secrets: the credential provider stores them in AWS Secrets Manager, performs the exchange, and returns a user-scoped token. AWS's security blog walks the full pattern with a named external system: an agent operating on Salesforce data, where the OBO-exchanged token lets Salesforce's own sharing rules enforce per-user record access, resolved via a Salesforce token-exchange handler mapping the inbound user identity. In the multi-tenant variant against Okta, AWS's implementation guide documents the sharp edge every adopter hits: Okta requires subject_token_type=access_token while AgentCore defaults to jwt, and the exchange fails with invalid_request until you override it per credential provider. Microsoft's Entra agent flows cover the same ground with Federated Identity Credentials, including a third mode where an agent gets its own user principal ("agents having their own mailbox") — a pragmatic concession that some systems simply will not accept non-human principals.
aws bedrock-agentcore get-resource-oauth2-token \
--resource-credential-provider-name sample-obo-provider \
--oauth2-flow ON_BEHALF_OF_TOKEN_EXCHANGE \
--scopes "sample-scope" \
--workload-identity-token "workload-access-token"
Honest weaknesses.
- IdP support is uneven and badly documented. The
subject_token_typemismatch above is one example; Microsoft implements its OBO flow on the RFC 7523 JWT-bearer grant rather than RFC 8693, so "we support token exchange" means different wire protocols per vendor. Budget integration time per IdP, not per pattern. - Nested
actchains grow with every hop, and RFC 8693 is explicit that only the current actor is an authorization input — so a resource server trusting the chain for access decisions is misreading the spec. The chain is for audit; policy evaluatessubplus current actor. (Okta uses thecidclaim, Auth0 caps nesting at five levels — implementations diverge on the same idea.) - Impersonation semantics are a footgun. If your exchange issues impersonation tokens, downstream sees "user did this" with no machine marker. For agents, always prefer composite/delegation tokens where the actor is distinguishable.
- Every hop adds a round-trip to the token endpoint. Deep agent-to-agent chains multiply AS load and latency; a fleet burst can turn your IdP into the bottleneck.
Who should skip: agents with no user mandate — a monitoring or indexing agent acting purely on its own behalf needs client credentials, not exchange. Also skip if your IdP has no token-exchange support at all: home-rolled "token forwarding with a header" is worse than a well-scoped static credential because it creates the illusion of delegation. The IETF is also drafting OAuth Transaction Tokens for agents, which add actor and principal context fields for exactly this call-graph problem — watch that space rather than building bespoke claim schemas.
Pattern 3 — MCP Authorization: What the Spec Covers, and What It Leaves Open
Agents reach tools through MCP, and the 2026-07-28 revision of the MCP authorization spec is far more prescriptive than most tutorial coverage admits. The normative core (we covered the transport-side changes in our stateless-spec production guide):
- Authorization servers MUST implement OAuth 2.1 with PKCE; clients MUST validate PKCE support from authorization-server metadata before starting a flow.
- MCP servers MUST implement Protected Resource Metadata (RFC 9728) and clients MUST use it to discover the authorization server — no hardcoded AS URLs.
- Clients MUST send RFC 8707 resource indicators so tokens are audience-bound to the specific MCP server; servers MUST reject tokens not issued for them.
- Token passthrough is explicitly forbidden: an MCP server proxying upstream APIs MUST obtain its own token from the upstream authorization server, never forward the inbound one.
- Client registration is shifting from Dynamic Client Registration (RFC 7591, retained for backwards compatibility) toward OAuth Client ID Metadata Documents.
- Refresh tokens are deliberately constrained — servers SHOULD NOT request
offline_access, which keeps MCP sessions from becoming standing credentials.
The real gaps are the parts the spec does not say:
- No client-credentials profile for autonomous agents. The core flows assume a human consent moment (authorization code + PKCE). A headless agent calling an MCP server has to ride a user-consented token or an out-of-spec arrangement — the project's own index lists SEP-1046, "Support OAuth client credentials flow in authorization," as an active proposal. Until it lands, autonomous-agent MCP deployments live in a spec gray zone.
- Bearer tokens are still replayable within their window. Audience binding stops a token being reused at other services, but a stolen token replays fine at its intended audience until expiry. The spec's security considerations name token theft; the mechanism (short TTLs, DPoP-style proof-of-possession) is left to implementers. The gateway-model proposal in MCP discussion #804 argues this is structural: the raw access token is exposed to server logic and the LLM itself, and prompt injection or a log line can leak it.
- Scopes are static strings; policy is not. The spec allows per-operation scope challenges, but a scope cannot answer "should this agent move this much money, right now, given its behavior in the last five minutes?" Discussion #804 proposes a gateway/PEP pattern — authenticate at the edge, mint a short-lived assertion, keep the primary token away from the agent entirely — and the honest criticism writes itself: a MITM gateway breaks end-to-end token binding and becomes both chokepoint and high-value target. The spec acknowledges the problem without prescribing the answer.
- Enterprise delegation arrives via extension, not core. The Enterprise-Managed Authorization extension profiles Cross App Access (the IETF ID-JAG draft) so an IdP brokers agent-to-MCP-server access with zero user interaction. Anthropic shipped this for Claude connectors ("starting with Okta," per their enterprise-managed auth article). It is the right pattern — but it is an extension, which means server support is optional and inconsistent.
Who should skip: stdio and in-cluster MCP servers with a trusted caller can read credentials from the environment per the spec's own guidance without standing up an OAuth 2.1 apparatus — do not build an AS for a tool server only your orchestrator calls. Conversely, if you operate MCP servers that any third-party agent can connect to, none of this is optional: unauthenticated tool servers with production scopes are the unpatched CVE you have not found yet.
Pattern 4 — SPIFFE/SPIRE: Attested Workload Identity Under the Fleet
Everything above still needs an answer to "how does the agent authenticate to the IdP in the first place?" The CNCF answer is SPIFFE: every workload gets a stable identity URI (a SPIFFE ID like spiffe://prod.example.com/ns/inference/sa/agent-worker), delivered as a short-lived X.509-SVID or JWT-SVID by a SPIRE agent that attests the workload from platform facts — namespace, service account, pod labels — instead of a pre-shared secret. There is nothing in the credential for an attacker to steal from your repo, and nothing to rotate by hand; X.509-SVIDs default to roughly an hour and rotate automatically.
Production precedent. Uber runs SPIRE across a fleet of over a quarter-million nodes in multiple clouds; their engineering post documents the scale problems that matter for AI fleets specifically — SPIRE Agent caching thousands of SVIDs per host group, solved with an LRU cache that let them register ~2.5× more workloads per host while cutting SPIRE Server CPU by 40%. Indeed runs a nested SPIRE topology (root CA in an infrastructure cluster, intermediates per Kubernetes cluster) across a 3,000-pod estate, and their post carries the warning every federation adopter should read first: AWS accepted about 100 JWT signing keys while Confluent accepted only a handful, and neither vendor documents the limit. And the vendor direction is telling: Google's Agent Identity is explicitly SPIFFE-based — every agent gets a SPIFFE ID with an auto-provisioned X.509 certificate (24-hour validity) and access tokens cryptographically bound to that certificate, so a stolen token cannot be replayed from outside the agent's runtime.
The bridge from SPIFFE to the OAuth world is JWT-SVIDs: the SPIRE OIDC Discovery Provider publishes a standard /.well-known/openid-configuration and JWKS for the trust domain, and any OIDC-federating system accepts JWT-SVIDs as issuer tokens. Anthropic documents exactly this for the Claude API — a SPIRE-issued JWT-SVID (audience https://api.anthropic.com) is exchanged at Anthropic's token-exchange endpoint for a short-lived API token, with SPIRE's five-minute default JWT-SVID TTL comfortably inside Anthropic's one-hour identity-token ceiling. That is the full pattern in one integration: attested workload identity in, scoped API credential out, zero static secrets anywhere in the chain.
spire-server entry create \
-spiffeID spiffe://prod.example.com/ns/inference/sa/agent-worker \
-parentID spiffe://prod.example.com/k8s-workload/cluster/nodes \
-selector k8s:ns:inference \
-selector k8s:sa:agent-worker \
-selector k8s:pod-label:app:inference-agent
Honest weaknesses.
- Operational weight. SPIRE Server/Agent fleets, registration hygiene, and trust-domain federation are a platform team's worth of work. Uber built a configuration-generator binary into a golden OS image to manage agent config drift across a quarter-million hosts — that is the level of investment the reference deployments imply.
- SPIFFE gives you workload identity, not agent task authorization. A SVID proves "this is the inference-agent pod in namespace inference" — it says nothing about which user's mandate this run executes. You still need Patterns 1–3 above; SPIFFE replaces the bootstrap secret, not the delegation chain.
- Federation limits are undocumented per vendor (Indeed's AWS/Confluent signing-key caps). Cross-cloud agent fleets need testing against every federating system, not just SPIRE-to-SPIRE.
- The attestation selectors you choose define your identity granularity. Namespace + service account is the common floor; if two different agents share a service account they share an identity, and your audit trail silently coarsens.
Who should skip: single-team fleets on one managed Kubernetes service — the cloud's native workload identity (EKS/GKE/AKS projected tokens) gives you 80% of the benefit with none of the control plane. SPIFFE/SPIRE earns its cost when you are multi-cloud, multi-cluster, or need one identity system across VMs and Kubernetes. If your agents all run inside one cluster and call one vendor API, a managed workload identity plus token exchange is the pragmatic stack.
Pattern 5 — A2A: Declarative Security Schemes, Out-of-Band Credentials
Agent-to-agent protocols (A2A) take the opposite tack from MCP: instead of specifying the OAuth machinery, A2A delegates. An A2A server publishes an Agent Card — a JSON document at a well-known URI — declaring which security schemes it accepts: API key, HTTP auth (basic/bearer), OAuth 2.0, OpenID Connect, or mutual TLS. Credential acquisition is explicitly out-of-band: the client agent obtains tokens "through processes external to the A2A protocol itself," and identity travels in HTTP headers, never inside the JSON-RPC payload. The security field composes schemes as an OR-of-ANDs — "callers must use OAuth, or an API key plus mTLS" — which is expressive enough for real enterprise posture.
{
"name": "compliance-reviewer",
"url": "https://agents.internal.example.com/a2a/v1",
"securitySchemes": {
"oauth": {
"oauth2SecurityScheme": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "https://auth.example.com/token",
"scopes": { "a2a:invoke": "Invoke this agent" }
}
}
}
},
"mtls": { "mtlsSecurityScheme": { "type": "mutualTLS" } }
},
"security": [ { "oauth": ["a2a:invoke"] }, { "mtls": [] } ]
}
Honest weaknesses.
- The spec defines transport auth, not delegation. Nothing in the A2A security model carries "agent A called agent B on behalf of user X" — you must layer RFC 8693 exchange or transaction tokens yourself across the A2A hop, and the spec will not help you.
- Out-of-band means out-of-band. How the client agent actually obtains and rotates credentials is unspecified, which in practice re-opens the door to exactly the static-key habits the rest of the stack exists to kill. The spec's own guidance ("generally NOT RECOMMENDED to include plaintext secrets in an Agent Card") is a hint at what people were doing.
- An Agent Card is only as trustworthy as its serving endpoint. A tampered card is a downgrade attack — the countermeasure (mTLS, network restrictions, authenticated extended cards) is specified but optional.
Who should skip: internal fleets where agents are built by one team — direct service-to-service auth (SPIFFE mTLS, Patterns 1–2) is simpler than discovering each other's cards. A2A's declarative model earns its keep when agents from different owners/vendors must interoperate without prior integration.
The Vendor Layer Is Converging — Fast
Eighteen months ago "agent identity" was a conference talk; as of October 2026 it is a SKU category with shipping GA products. The convergence is on the same primitives this guide describes — OAuth exchange, SPIFFE attestation, IdP-brokered consent:
| Vendor | Product (Oct 2026) | Standards anchor | Commercial shape | The honest catch |
|---|---|---|---|---|
| Microsoft | Entra Agent ID (in Agent 365 / M365 E7) | OAuth client credentials + OBO via Federated Identity Credentials | Agent 365 $15/user/mo standalone; M365 E7 $99/user/mo; foundational agent capabilities free with any Microsoft Cloud subscription (as of Oct 2026, microsoft.com) | Licensed per human user, not per agent — cheap at scale, but full governance rides on an E5-class baseline |
| Okta | Okta for AI Agents + Agent SSO | Cross App Access (IETF ID-JAG draft) + MCP Enterprise-Managed Authorization extension | Agent SSO included in core Okta SSO; the full for-AI-Agents suite is a separate subscription (no public list price) | The free tier governs only agents that speak Cross App Access; shadow-agent discovery and the runtime kill switch land through the paid suite (gateway GA planned Q3 2026 and expanded kill switch Q4 2026, per Oktane announcements) |
| AWS | Bedrock AgentCore Identity | RFC 8693 token exchange + RFC 7523 JWT-bearer OBO | Usage-based AWS service pricing — verify current rates on the Bedrock AgentCore pricing page before committing | Per-IdP integration variance is real (the Okta subject_token_type mismatch ships in the default config); 14 GA Regions at announcement, not global |
| Google Cloud | Agent Identity (with Agent Gateway / Gemini Enterprise) | SPIFFE IDs, cert-bound tokens, mTLS + DPoP, IAM/PAB/VPC-SC integration | Bundled with Google's agent platform rather than metered standalone | Deeply GCP-anchored — the strongest token-binding story of the four, and the least portable |
Two things stand out. First, per-user licensing (Microsoft) vs per-subscription suites (Okta) is a real cost-model fork — on a 1,000-employee estate, Agent 365 standalone is $15,000/month at list, which is $180,000/year before the E5-class baseline most of its advanced features assume. Second, every one of these products assumes agents connect through their gateway or registry; if your fleet spans clouds, the IdP-neutral patterns in this guide (raw RFC 8693, SPIFFE) are the only layer that does not lock you to one vendor's agent platform. The managed offerings are a control plane on top of the same primitives — worth renting when your estate is single-vendor, worth bypassing when it is not.
The Zero-Trust Checklist for Agent Fleets
Compressed to what actually changes in production:
| Control | API-key fleet | Zero-trust agent fleet |
|---|---|---|
| Bootstrap credential | Static key in env/secrets store | Attested identity (SPIFFE SVID or managed workload identity) — no secret to leak |
| Session credential | Same key, forever | Short-lived, audience-bound OAuth token per downstream system |
| Delegation | None — agent borrows the user's power invisibly | RFC 8693 composite tokens: sub = mandate, act = actor, nested chain for audit |
| Downstream validation | "Key matches → allow" | Audience check (RFC 8707), current-actor policy, per-request scopes where supported |
| Revocation | Hunt the key across every consumer | Deactivate the agent identity at the IdP/gateway — one switch, sessions die at next token refresh |
| Audit question | "Someone with this key did it" | "Agent X, actor Y, acting for user Z, audience W, at time T" |
| Ownership | Nobody | A named human owner per agent identity — the control both Okta and Microsoft lead with |
None of this requires adopting every pattern at once. The honest sequencing for most fleets: kill shared static keys first (client credentials or managed workload identity), add token exchange where agents act on user data, adopt the MCP authorization mandates before your tool servers multiply, and reserve SPIFFE/SPIRE for the multi-cluster reality that makes attestation worth its operational price. What is not on the table is standing pat — the window between agents gaining production access and organizations giving them real identities is exactly where the incidents Okta's research measured are happening.