Classic painting used as the article cover
← Back to blog

IDENTITY

OAuth 2.1 for Agent Builders: What Changed and Why It Matters

OAuth 2.1 folds a decade of security guidance into one spec. For agent builders the load-bearing parts are discovery, runtime client registration, and audience-restricted tokens.

Paulina XuAug 24, 202618 min
OAuthIdentityProtocols

TL;DR

Most of what people call the "OAuth 2.1 checklist" is compliance homework, and one line in RFC 8707 is the exception that actually matters. PKCE for effectively all clients, exact redirect matching, no implicit grant: this is a decade of security guidance finally collected into one document, and a client that was already careful barely has to change.

The part that actually matters for an agent is audience restriction: sending a resource parameter on every token request so the token only works at the server it was issued for. MCP's 2026-07-28 specification turned that from a SHOULD into a MUST, on both sides of the exchange.

We think that is where the review time should go, not the consolidation list. A process that reads untrusted text and holds several tokens at once needs its blast radius kept small more than it needs one more layer against theft.

PKCE and sender-constraining still matter. This is about where the marginal hour goes: send resource on every request, validate aud on every arrival, and stop building new clients around Dynamic Client Registration. MCP has already moved on to Client ID Metadata Documents.

Overview

OAuth 2.1 is a consolidation, not a new protocol: the OAuth 2.0 core, the native-apps profile, PKCE, the browser-based-apps guidance, bearer token usage, and the OAuth 2.0 Security Best Current Practice, folded into one document that replaces and obsoletes RFC 6749 and RFC 6750. The current revision is draft-ietf-oauth-v2-1-15, published March 2026. Most of what's in it is old news dressed up as a spec. Engineers who've been around OAuth for a few years already know the shape: PKCE everywhere, implicit gone. Few have read the normative text closely enough to say why.

That rough sense builds a web app. It doesn't build an agent. An agent client has properties OAuth 2.0 never imagined: it may not exist until a user asks for it, it may need to reach a resource server whose authorization server it has never seen, and it will often hold tokens for several unrelated services at once, in one process. Three parts of the modern OAuth surface do the real work for that shape: metadata discovery (RFC 8414 and RFC 9728), client identity established at runtime (RFC 7591, and increasingly the Client ID Metadata Document draft), and resource indicators (RFC 8707), which is what makes audience restriction possible.

Authorization-code flow, refresh-token mechanics, and token storage hygiene are covered elsewhere on this site; assume the reader already has them. Whether OAuth is sufficient for agents at all, and how MCP's permission model surfaces to users, are separate questions for separate posts. What's here is what the standards say on the wire, with section numbers, and which of those requirements actually changes an agent's blast radius.

Consider a release-readiness agent at a mid-size payments company. Every night it assembles a go/no-go summary by querying three MCP servers: the issue tracker, the CI system, and the log-search service. Three resource servers, three authorization servers, one process. That agent is the worked example throughout.

We think the single most consequential control in OAuth 2.1, for agents specifically, is audience restriction. Every other requirement raises the cost of stealing a token. Audience restriction lowers what a stolen token is worth once someone has it, which matters more when the thing holding the token is a process that also reads untrusted text for a living.

What OAuth 2.1 Consolidates

Section 10 of the draft carries a non-normative list of changes from OAuth 2.0. It's worth reproducing accurately, because most summaries of OAuth 2.1 get at least one of these subtly wrong:

  • The authorization code grant is extended with PKCE (RFC 7636), such that the default way to use the code grant requires the PKCE parameters.
  • Redirect URIs must be compared using exact string matching, per Section 4.1.3 of RFC 9700.
  • The Implicit grant (response_type=token) is omitted, per Section 2.1.2 of RFC 9700.
  • The Resource Owner Password Credentials grant is omitted, per Section 2.4 of RFC 9700.
  • Bearer tokens may not be passed in the query string of a URI, per Section 4.3.2 of RFC 9700.
  • Refresh tokens for public clients must either be sender-constrained or one-time use, per Section 4.14.2 of RFC 9700.
  • The token request containing an authorization code no longer carries redirect_uri.
  • Authorization servers must support client credentials in the request body.

Read that list as compliance homework, not new thinking. A client that already does PKCE with an S256 challenge and rejects redirect URIs on anything less than an exact match has done most of the work already; OAuth 2.1 mostly writes down what a careful implementation was supposed to do anyway. We wouldn't spend a design review here. The two items below are the ones actually worth the meeting.

The removal of redirect_uri from the token request (Section 10.2) is a consequence, not a goal: the parameter existed to prevent authorization-code injection, PKCE now prevents that, so the parameter has nothing left to do. An authorization server that wants to serve both OAuth 2.0 and 2.1 clients still MUST allow clients to send it and MUST enforce it as RFC 6749 describes.

The refresh-token rule is narrower than it is usually quoted. OAuth 2.1 Section 4.3.1 requires authorization servers to use one of two named methods to detect refresh-token replay for public clients: sender-constrained refresh tokens bound cryptographically to a client instance via DPoP or mTLS, or refresh-token rotation, in which a new refresh token accompanies every refresh response and the previous one is invalidated. The spec is explicit about rotation's tradeoff: on replay, the authorization server learns a breach occurred but cannot tell which party submitted the invalidated token, so it revokes the active refresh token and the whole authorization grant. That stops the attack, but at a cost. The legitimate client has to obtain a fresh grant, and for an unattended nightly agent that means a human in the loop at 3 a.m. Worth knowing before you pick rotation over sender-constraining.

RFC 9700 is worth reading directly rather than through the 2.1 draft. It is BCP 240, published January 2025, and it updates RFC 6749, 6750, and 6819. Nearly every normative restriction in OAuth 2.1 traces to a numbered section in it.

PKCE, Precisely

The common summary, "PKCE is required for all clients in OAuth 2.1, including confidential ones," is close enough to be useful. It's also wrong in a way that matters if you operate an authorization server.

The actual text in Section 4.1.1 lists code_challenge as "REQUIRED unless the specific requirements of Section 7.5.1 are met." Section 7.5.1.1 defines the carve-out precisely: code_challenge and code_verifier are required, and authorization servers MUST enforce their use unless both of the following hold. The client is a confidential client. And, in that specific deployment and that specific request, the authorization server has reasonable assurance the client implements the OpenID Connect nonce mechanism correctly. Even then, the draft still calls using PKCE RECOMMENDED.

The draft also explains why it prefers PKCE over nonce even where both are available, and the reasoning is directly relevant to agents. Relying on the client to validate nonce means the authorization server has no way to confirm the client actually protected itself; an injected authorization code would still be exchanged and tokens still issued, with the client only rejecting the ID token afterward. With PKCE, the authorization server recognizes the injection pre-emptively and issues nothing. For a client population as heterogeneous as agent runtimes, "the authorization server can verify the countermeasure" is worth much more than "the client promises to check."

The draft appends a historical note that's easy to miss. PKCE was designed to protect native apps from code exfiltration, but the draft is clear that all kinds of OAuth clients, web applications and other confidential clients included, are susceptible to authorization-code injection, which PKCE solves. Confidential clients were never exempt from the underlying problem.

Discovery: How an Agent Finds the Rules

An agent that only ever talks to services chosen at build time can hardcode endpoints. The release-readiness agent cannot, because the set of MCP servers it queries is configuration, not code, and each of those servers may sit behind a different authorization server.

Two metadata documents solve this, and they discover in opposite directions.

Protected resource metadata (RFC 9728, April 2025) is served by the resource server at /.well-known/oauth-protected-resource and tells a client which authorization servers are acceptable for this resource:

http
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp.ci.internal

HTTP/1.1 200 OK
Content-Type: application/json

{
  "resource": "https://mcp.ci.internal",
  "authorization_servers": ["https://sso.example.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["builds:read", "pipelines:read"],
  "resource_documentation": "https://mcp.ci.internal/docs"
}

A resource server can also point at this document from a challenge, using the resource_metadata parameter RFC 9728 defines for WWW-Authenticate. That turns a 401 into a self-describing invitation to authorize:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata=
  "https://mcp.ci.internal/.well-known/oauth-protected-resource",
  scope="builds:read"

The validation rule in Section 3.3 is the part to implement carefully. The resource value returned MUST be identical to the resource identifier into which the well-known path suffix was inserted; and if the document was fetched from a URL supplied via resource_metadata, the returned resource value MUST be identical to the URL the client used to reach the resource server. If they differ, the data MUST NOT be used. RFC 9728 says plainly that these checks are what thwart impersonation attacks. An agent that fetches metadata from whatever URL a 401 hands it, without comparing the round trip, has built a redirection primitive for an attacker.

Authorization server metadata (RFC 8414, June 2018) runs the other way, served at /.well-known/oauth-authorization-server, listing the endpoints and capabilities the client needs:

http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "issuer": "https://sso.example.com",
  "authorization_endpoint": "https://sso.example.com/authorize",
  "token_endpoint": "https://sso.example.com/token",
  "registration_endpoint": "https://sso.example.com/register",
  "jwks_uri": "https://sso.example.com/jwks.json",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["private_key_jwt"],
  "scopes_supported": ["builds:read", "pipelines:read"],
  "client_id_metadata_document_supported": true
}

That last field comes from the Client ID Metadata Document work, not RFC 8414, and it's the hinge of the next section.

Auth ServerMCP Server (RS)AgentAuth ServerMCP Server (RS)Agentrequest, no token401 + resource_metadata + scopeGET /.well-known/oauth-protected-resourceresource, authorization_serversverify resource == URL usedGET /.well-known/oauth-authorization-serverendpoints, PKCE methods, CIMD supportestablish client identity (CIMD or DCR)client_idtoken request + code_verifier + resourcetoken with aud = this RS onlyrequest with tokenvalidate aud == selfresult

Figure 1 — Discovery, runtime client identity, audience-restricted token, single-resource call.

Runtime Registration for Clients That Did Not Exist Yesterday

OAuth's original model assumes a human registered the client with the authorization server ahead of time and copied a client_id into a config file. Agent ecosystems break that assumption from both ends: the client may be a desktop tool a user installed an hour ago, and the authorization server may belong to an organization that has never heard of that tool.

Dynamic Client Registration (RFC 7591, July 2015) was the first answer. A client POSTs its own metadata and receives credentials:

http
POST /register HTTP/1.1
Host: sso.example.com
Content-Type: application/json
Accept: application/json

{
  "client_name": "Release Readiness Agent",
  "redirect_uris": ["http://127.0.0.1:7391/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "application_type": "native"
}
http
HTTP/1.1 201 Created
Content-Type: application/json

{
  "client_id": "s6BhdRkqt3",
  "client_id_issued_at": 1787000000,
  "redirect_uris": ["http://127.0.0.1:7391/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none"
}

This works, and it has a well-understood problem: it writes attacker-controllable records into the authorization server's client table. Anyone who can reach an open registration endpoint can create clients, choose display names that appear on consent screens, and generate registration volume nobody asked for. It also gives every client a per-authorization-server identity that has to be stored and correctly keyed. That's why the MCP specification requires clients persisting DCR credentials to key them by the issuing authorization server's issuer identifier, and to re-register rather than reuse credentials if the authorization server changes.

The current direction is different. Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document-02, 6 July 2026, by Aaron Parecki and Emelia Smith) inverts the flow: the client's client_id is an HTTPS URL with a path component, and that URL serves the metadata. Nothing is written to the authorization server; it fetches, caches, and can apply a trust policy to the domain.

json
{
  "client_id": "https://agents.example.com/release-readiness/client.json",
  "client_name": "Release Readiness Agent",
  "client_uri": "https://agents.example.com/release-readiness",
  "redirect_uris": ["http://127.0.0.1:7391/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

The MCP 2026-07-28 specification has already made this shift, and it's the single change most likely to catch out someone who last read MCP authorization a few months ago. We think it's worth stating plainly: if you are still building a new agent client toward Dynamic Client Registration as the primary path, you are building toward the fallback. The spec now defines three registration mechanisms in priority order. Pre-registered credentials come first. Client ID Metadata Documents come second, where the authorization server advertises client_id_metadata_document_supported. DCR comes third, and the spec labels Dynamic Client Registration deprecated, retained only for authorization servers that don't support CIMD. Servers and clients "SHOULD" support CIMD; they "MAY" support DCR. The payoff the spec calls out is portability: a CIMD-based client ID is a self-hosted URL resolved on demand, so nothing needs re-registering when the authorization server changes.

If you're building an agent client today, implement CIMD as the primary path and DCR as the fallback. If you're building an authorization server for agent traffic, remember that CIMD makes you an HTTP client fetching attacker-nominated URLs. That's its own threat surface. The draft's Section 6 and the MCP security considerations both address it, and both are worth reading before you ship the fetcher.

Audience Restriction Is the Control That Matters

Everything covered so far reduces the probability of a token being stolen. Audience restriction reduces what a stolen token is worth once someone has it, and nothing else discussed here does that.

Resource Indicators for OAuth 2.0 (RFC 8707, February 2020) defines a single request parameter. resource indicates the target service to which access is being requested. Its value MUST be an absolute URI with no fragment component, it SHOULD NOT include a query component, and multiple resource parameters MAY be sent to indicate a token intended for use at multiple resources. A new error code, invalid_target, tells the client the requested resource is invalid, missing, unknown, or malformed, or that it asked for an invalid combination of resource and scope. The normative payoff is one sentence: the authorization server SHOULD audience-restrict issued access tokens to the resources indicated by the resource parameter, communicated in a JWT via the aud claim.

RFC 8707 also gives specific guidance on granularity that people routinely ignore. Use the base URI of the API unless you know better. Its own example is SCIM: given endpoints at /scim/Users, /scim/Groups, and /scim/Schemas, the client should use https://apps.example.com/scim/ so the token is valid across the API.

For the release-readiness agent, the token request looks like this:

http
POST /token HTTP/1.1
Host: sso.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=https%3A%2F%2Fagents.example.com%2Frelease-readiness%2Fclient.json
&resource=https%3A%2F%2Fmcp.ci.internal

One resource value, one audience, one blast radius. The agent repeats the exchange for the issue tracker and the log service, and ends up holding three tokens that are each useless at the other two services.

Why this is the control that matters is easiest to see by removing it. RFC 9700 Section 4.10.2 walks through the counterfeit-resource-server case: if the audience is the actual URL the client sends requests to, a token phished by a counterfeit carries that counterfeit's URL as its audience, and replaying it at the legitimate resource server fails audience validation. Section 2.3 generalizes the principle. Access tokens SHOULD be audience-restricted to a specific resource server, or failing that a small set, and every resource server is obliged to verify on every request that the token was meant for it, and MUST refuse if it was not.

The agent case sharpens this considerably. A prompt-injection payload sitting in a build log is a plausible way for an attacker to get an agent to make a request it should not make. If that agent holds one broad token, the attacker's target list is every service that token is good at. If it holds three audience-restricted tokens, an injected instruction to exfiltrate customer records from the CRM fails at the CRM's audience check even if the agent is fully persuaded to try.

No

No

No

No

Yes

Yes

Yes

Yes

Token stolen from agent process

Audience restricted?

Replay at CI API

Replay at issue tracker

Replay at log search

Replay at CRM

CI API: aud matches, accepted

Issue tracker: aud mismatch, 401

Log search: aud mismatch, 401

CRM: aud mismatch, 401

Figure 2 — What an unrestricted token buys an attacker, versus an audience-restricted one.

Sender-Constrained Tokens, Honestly

OAuth 2.1 Section 1.4.3 says authorization and resource servers SHOULD use mechanisms for sender-constraining access tokens, naming DPoP (RFC 9449, September 2023) and mutual TLS (RFC 8705, February 2020), and pointing at RFC 9700 Section 4.10.1. A sender-constrained token obliges the presenter to demonstrate knowledge of a secret, so possession of the token alone is insufficient.

The two mechanisms buy different things.

mTLS (RFC 8705)DPoP (RFC 9449)
LayerTransportApplication
BindingX.509 client certificatePer-request signed proof JWT
Key managementCertificate issuance and rotationClient-generated key pair
Usable from browser JavaScriptNoYes
Multi-audience reuseA client can use one token at several resource serversProof is bound per request
Replay detectionImplicit in TLSExplicit, including for refresh tokens

RFC 9700 Section 4.10.2 notes an interaction that is easy to miss: audience restriction requires no client-side cryptography, but because every token is bound to one resource server, a client talking to several must obtain a separate token for each. mTLS doesn't have that property. It lets a client use one token at multiple resource servers. These are different tradeoffs, not a ranking, though for an agent holding tokens for many services the per-resource-token pattern is usually what you want anyway.

On adoption, the honest position is that sender-constraining is available but not ambient. DPoP has shipped in production identity platforms including Auth0, Okta, and Keycloak. mTLS has a longer history but requires certificate lifecycle machinery many teams lack, and is not something browser-based JavaScript can drive. Neither is a default you can assume a counterparty supports, which is why OAuth 2.1's requirement is SHOULD rather than MUST. It's also why audience restriction, needing only a claim set on issue and a claim check on arrival, remains the control with the best ratio of protection to deployment cost.

If we were prioritizing, in order: audience-restrict everything first, then add PKCE and exact redirect matching if you somehow haven't, then pursue sender-constraining as your authorization server and counterparties support it. Reversing that order buys less for more work.

What MCP 2026-07-28 Requires

The MCP 2026-07-28 specification makes all of this concrete rather than aspirational. It takes normative positions in the exact places OAuth leaves latitude.

A protected MCP server acts as an OAuth 2.1 resource server, and an MCP client acts as an OAuth 2.1 client. Authorization remains OPTIONAL for MCP implementations overall, but HTTP-transport implementations SHOULD conform. STDIO implementations SHOULD NOT; they retrieve credentials from the environment instead.

The discovery requirements are hard. MCP servers MUST implement RFC 9728 protected resource metadata, and MCP clients MUST use it for authorization server discovery. MCP authorization servers MUST provide at least one of RFC 8414 metadata or OpenID Connect Discovery 1.0, and MCP clients MUST support both mechanisms. Authorization servers MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.

The resource-indicator requirements go further, past OAuth's SHOULD. MCP clients MUST implement RFC 8707. The resource parameter MUST be included in both authorization requests and token requests, MUST identify the MCP server the client intends to use the token with, and MUST use the canonical URI of that server. MCP clients MUST send this parameter regardless of whether authorization servers support it. There's no negotiation and no graceful degradation. Send it always.

On the receiving side, MCP servers MUST validate that access tokens were issued specifically for them as the intended audience. Clients MUST NOT send tokens to an MCP server other than ones issued by that server's authorization server. Servers MUST only accept tokens valid for use with their own resources, and MUST NOT accept or transit any other tokens. That last clause is the formal prohibition on token passthrough. An MCP server may not forward a token it received to a downstream service. That's the confused-deputy pattern, and it's what turns a well-meaning intermediary into an amplifier.

The spec also adopts RFC 9207 issuer identification. Before redirecting, a client MUST record the issuer from the validated authorization server metadata alongside the PKCE verifier, and on receiving the authorization response it MUST validate iss before transmitting the code to any token endpoint. Comparison is simple string comparison. Clients MUST NOT normalize case, default ports, trailing slashes, or percent-encoding first. The validation applies to error responses too: on mismatch a client MUST NOT act on or even display error, error_description, or error_uri.

Finally, scope handling became a protocol conversation rather than a guess. Servers SHOULD include a scope parameter in the WWW-Authenticate header indicating what the resource needs, and clients MUST treat challenged scopes as authoritative for the current operation while MUST NOT assuming any set relationship to scopes_supported. When re-authorizing, clients SHOULD union the challenge scopes with previously requested ones so other operations do not break. A runtime insufficient-scope error is a 403 carrying error="insufficient_scope" and the required scopes, and servers SHOULD emit all of them in one challenge rather than trickling them out per round trip.

For the release-readiness agent this lands somewhere pleasant. It starts with builds:read. The night it first tries to annotate a failing pipeline, the CI server returns a 403 naming pipelines:write, the agent steps up with the union of what it had and what it needs, and the resulting token still names exactly one audience.

Getting audience restriction right at issue time is necessary. It says nothing about what happens to the token afterward, whether it enters the model's context window or whether anyone can reconstruct which identity performed a given action. Agentic Fabriq's default proxy mode keeps credentials in a vault and injects them into the outbound call at call time, so the raw token never reaches the agent's reasoning loop, and every request carries two identities, the agent's and the user's. Audience restriction narrows what a leaked token can reach; keeping the token out of the loop narrows the chance of the leak.

Conclusion

OAuth 2.1 is a tidying-up, and most of what it tidies is a decade old. PKCE for effectively all clients, exact redirect matching, no implicit grant, no password grant, no bearer tokens in query strings, replay detection on public-client refresh tokens: none of it is new thinking. If your agent client doesn't do all of it, that gap is technical debt, not a research problem.

The agent-specific work is elsewhere: treating discovery as a runtime operation with validation rules you actually implement, establishing client identity for software that appears without prior arrangement (Client ID Metadata Documents first, Dynamic Client Registration as a fallback, at least in MCP), and above all sending resource on every request and validating aud on every arrival. MCP's 2026-07-28 specification turned that last one from a SHOULD into a MUST on both sides. We think that's the clearest signal available of where the community has decided the leverage actually is.

A token that names its audience is a token whose theft is survivable. Everything else in OAuth 2.1 makes theft less likely. Only audience restriction makes it less consequential. For a process that reads untrusted input as part of its normal operation, planning for the survivable case is just good design, not pessimism.

Sources