OAuth has always had a little secret… The client is supposed to just “know” where to get a token.
The client had to just know which authorization server to go to with what scopes and know how to present the token correctly.. Somebody had to read the API docs, copy a few values into a config file, and they could call it an integration.
This falls apart the second the client is an agent that has never seen that API before.
This is the gap Protected Resource Metadata (PRM) fills (RFC 9728). If you’ve touched MCP authorization at all, you SHOULD BE using it.
PRM Doesn’t Authorize Anything
Let’s nip something in the bud first. PRM is not an authorization mechanism. It doesn’t issue tokens, validate tokens, or decide who gets in. It’s simply a JSON document that says “here’s how you talk to me.”
If you know RFC 8414 (Authorization Server Metadata), PRM is its mirror. RFC 8414 lets an authorization server describe itself. PRM actually lets the resource server describe itself.
What the Document Looks Like
The metadata lives at a well-known URI derived from the resource identifier, just like most publicly accessible OAuth endpoints. If your resource is https://api.example.com/mcp, the metadata should be at https://api.example.com/.well-known/oauth-protected-resource.
A minimal document would look like this:
{
"resource": "https://api.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://api.example.com/docs"
}
resource: required. Must exactly match the identifier the client used to find the document. This is the anti-spoofing check.authorization_servers: issuer identifiers for the AS(s) that issue tokens for this resource. The client picks one, fetches its RFC 8414 metadata, and carries on.scopes_supported: the scopes that are accepted at this endpoint.bearer_methods_supported: header, body, or query. (If you see query… run.)- Sender-constraint flags like
dpop_bound_access_tokens_requiredandtls_client_certificate_bound_access_tokensare also optional, so the client knows up front whether a plain bearer token is going to get bounced.
How the Flow Works
Here’s the whole kit and kabootle, start to finish:
- The client calls the resource (probably with no token).
- The resource returns a 401 with a pointer:
WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp". e.g. “Go this way to see the PRM” - The client fetches the PRM document and checks that
resourcematches what it called. - The client picks an AS from
authorization_serversand fetches that AS’s metadata. e.g. “Go this way to authenticate” - The client runs a normal OAuth flow, passing the RFC 8707
resourceparameter so the token is minted for this specific resource. - The client retries the original call with the token.
Note that this flow occurs with zero preconfiguration. The client went from “never heard of ya” to “Oh, what’s up Steve?” using nothing but the 401.
Why It Took MCP to Matter
The idea behind PRM kicked around the OAuth working group for years before it became an RFC. Traditional integrations were human-wired and discovery is a ‘nice to have’ when a person is in the loop.
MCP flipped that idea on it’s head. Now you have generic clients (an AI assistant, an IDE, an agent framework, etc.) connecting regularly to servers they’ve never seen before, and lucky for us, preconfiguring the right AS for every MCP server on the freaking planet isn’t a thing. This leaves us with one viable option: The client has to discover it at runtime.
So the MCP spec (I believe starting in 2025) made PRM mandatory for MCP servers and in the same move cleaned up an early design that blurred the MCP server and the authorization server together.
So we’re all clear on this: The MCP server is a resource server only. It points at its AS. Overnight, every MCP server author was expected to implement an RFC most of them had never heard of. To that I say welcome to identity. We have free snacks.
PRM Is Just a Pointer, Never a Trust Anchor
Ok. So.
The resource server is now telling the client where to go get credentials. Just think about that for a second.. A malicious resource can list any AS it wants, including your real corporate IdP. Your user will then see a perfectly legitimate login page because it is a legitimate login page.
What actually saves you here is audience binding. If the client sends the RFC 8707 resource parameter and the AS honors it, the token minted during that “legit” login is only good at the hostile resource. Useless anywhere else. If your AS ignores the resource parameter and hands out tokens, congratulations.. You set up PRM to be a phishing assistant.
Same logic on the AS side. A resource listing your AS in its metadata is the resource’s claim, not your AS’s agreement. The AS still decides which resources it will mint tokens for like it always has.
Where People Get It Wrong
- Wrong path. Publishing at
/mcp/.well-known/oauth-protected-resource. The well-known segment goes at the origin root; the resource path gets appended after it if necessary. - Skipping the
resourcematch. Clients that fetch the document and don’t compareresourceto the URL they called have thrown away the one built-in spoofing defense. - Treating PRM as trust. It’s a forwarding address. Whether you should talk to that resource at all is a separate decision that happens on the AS.
- Ignoring RFC 8707 on the AS. Discovery without audience-restricted tokens is how you end up with tokens that work everywhere.
- Hardcoding the metadata URL. This is fine for dev or a demo. Read it from
WWW-Authenticatein anything real. That header is also how a resource signals its metadata changed.
Bias check: I work at Okta and spend a lot of my day in MCP auth, so I’m not exactly neutral on whether this stuff matters. (It does.)
TL;DR
- PRM (RFC 9728) lets an API tell clients which AS to use and how to present tokens. It’s discovery, not authorization.
- The 401 with
resource_metadatainWWW-Authenticateis the whole bootstrap. No preconfiguration needed. - MCP made it mandatory because generic clients can’t be pre-wired to every server. That’s why you’re suddenly hearing about it.
- PRM is a pointer, not a trust anchor. A hostile resource can point at your real IdP.
- Pair it with RFC 8707 audience binding, or don’t bother doing it at all.