OAuth·for·Agents

Scenario 02 · adds runtime discovery

The agent meets a service nobody registered with

Mid-conversation, someone gives the agent a URL: an MCP server run by a company that has never heard of it. No developer portal step, no human free to go do one. Two strangers have to bootstrap a trust relationship from a hostname, in under a second, and then get a user's consent.

This is the actual new problem

The two rungs below assumed the agent already knew where to authenticate and had an identifier there, which came from a world where a developer decided on an integration months in advance. It's the assumption agents break hardest, and the one place OAuth needed a new part.

Two things have to happen at runtime:

  • Find the authorization server. The agent has a resource URL and nothing else. Which authorization server protects it?
  • Get an identity there. The authorization server has never seen this client. What identifier does it use, and how does it know anything about it?

The first was solved in 2025. The second is what Client ID Metadata Documents are for.

Part one

Discovery, from a bare URL

The agent calls the resource with no credentials. The resource replies 401 and tells it where to look: a WWW-Authenticate header pointing at its protected resource metadata (RFC 9728).

POST /mcp HTTP/1.1
Host: mcp.acme-crm.com

→ 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata=
  "https://mcp.acme-crm.com/.well-known/oauth-protected-resource"
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp.acme-crm.com

→ 200 OK
{
  "resource": "https://mcp.acme-crm.com/mcp",
  "authorization_servers": ["https://login.acme-crm.com"],
  "scopes_supported": ["crm.contacts.read", "crm.deals.write"],
  "bearer_methods_supported": ["header"],
  "dpop_bound_access_tokens_required": true
}

From there, RFC 8414 gets the agent the authorization server's endpoints and capabilities. Two well-known documents, and the agent knows how to authenticate to a service it learned about ten milliseconds ago.

Why the resource points at the AS, not the other way round

It matters that the resource declares which authorization server protects it. An API can be protected by a server it doesn't operate, tokens are audience-bound to the resource, and an agent can verify the token it's about to send was issued for the thing it's calling.

Part two

Getting an identity without registering

There are three ways for a never-before-seen client to have a client ID. They are not equally good.

Dynamic Client Registration

RFC 7591 is what the first wave of MCP servers used. The costs show up at scale: an unauthenticated write endpoint anyone can spray, a row per agent instance per server, and a newly minted, locally meaningless identifier. The consent screen says whatever the agent claims, because nothing authenticates the request.

Client ID Metadata Documents

The client's ID is an HTTPS URL serving the client's own metadata, and the authorization server fetches it. That's the whole mechanism, and it inverts the problem: the client has an identity and the server looks it up.

// client_id: https://agent.example.com/clients/research-assistant

GET /clients/research-assistant HTTP/1.1
Host: agent.example.com

→ 200 OK
Content-Type: application/json
Cache-Control: max-age=3600

{
  "client_id": "https://agent.example.com/clients/research-assistant",
  "client_name": "Research Assistant",
  "client_uri": "https://agent.example.com/",
  "logo_uri": "https://agent.example.com/logo.png",
  "redirect_uris": ["https://agent.example.com/callback"],
  "jwks_uri": "https://agent.example.com/jwks.json",
  "token_endpoint_auth_method": "private_key_jwt",
  "grant_types": ["authorization_code", "refresh_token"]
}

The properties that make this the right primitive for agents:

  • The identifier means the same thing everywhere. The same client ID at every authorization server on earth, with no bilateral setup.
  • It carries provenance a human can read. The consent screen can show the hostname, which is the one security signal users are actually trained on.
  • There is no write endpoint. Nothing to spam, nothing to store per client, nothing to garbage-collect.
  • It's revocable and reputable. Serve a 404 and the client is gone. Block a hostname and you've blocked a publisher, not a UUID.
  • No shared secret exists. The document publishes a jwks_uri, and the spec forbids client_secret and symmetric methods. Nothing in it is worth stealing.
If you're implementing the server side

You are now making outbound HTTP requests to URLs supplied by strangers. Block special-use and internal addresses, cap the response size (the draft suggests 5 KB), require a 200 and reject every other status, honour cache headers but don't cache failures, and re-check when a document's keys or redirect URIs change. This is the SSRF surface the mechanism buys you, and it is a known, bounded one.

Pre-registration

Still correct where it's possible. An enterprise can pre-register a client ID URL to grant it elevated trust: a stable identifier that works everywhere, plus an explicit local decision about which publishers get more than the baseline.

Putting it together

Zero to authorized

Agent meets an MCP server for the first time
  1. Agent→MCP Server

    Calls it. Gets 401 with a resource_metadata pointer.

  2. Agent→MCP Server

    Fetches protected resource metadata. Learns the authorization server, the available scopes, and that DPoP is required.

  3. Agent→Authorization Server

    Fetches /.well-known/oauth-authorization-server. Confirms the server supports client ID metadata documents and DPoP.

  4. Agent→Authorization Server

    Starts an authorization request using its own URL as client_id, with PKCE, and resource naming the MCP server. No registration call happens at any point.

  5. Authorization Server→itself

    Fetches the client ID URL, validates the document, and renders a consent screen showing the client name, logo, and the hostname it came from.

  6. Authorization Server→Agent

    User approves. Authorization code comes back with an iss parameter the agent verifies (RFC 9207). That's the defense against mix-up attacks when an agent is talking to several servers at once.

  7. Agent→MCP Server

    Exchanges the code for a DPoP-bound token audience-restricted to this MCP server, and makes the call it was trying to make seven steps ago.

POST /par HTTP/1.1
Host: login.acme-crm.com
Content-Type: application/x-www-form-urlencoded

response_type=code
&client_id=https%3A%2F%2Fagent.example.com%2Fclients%2Fresearch-assistant
&redirect_uri=https%3A%2F%2Fagent.example.com%2Fcallback
&resource=https%3A%2F%2Fmcp.acme-crm.com%2Fmcp
&scope=crm.contacts.read
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256

Two attacks this rung has to answer

The confused deputy

An agent holds tokens for six servers, and a malicious one tricks it into sending the wrong token. Resource indicators audience-bind each token at issuance, so the wrong one doesn't work: a class of agent bug becomes a rejected request.

The mix-up

An agent starts a flow with one authorization server and gets a response that appears to come from another. Issuer identification puts the issuer in the response and the client checks it against the one it started with. Small parameter, whole attack class.

Where MCP landed

The 2026-07-28 revision of the Model Context Protocol authorization specification makes MCP servers OAuth 2.1 resource servers, requires protected resource metadata, resource indicators and issuer validation, and makes client ID metadata documents the preferred way for clients to identify themselves.

Worth noticing as a data point: the largest new agent protocol looked at this problem, and its answer was existing OAuth building blocks plus one new draft.