Winslow Homer watercolour of a boy waiting beside a beached dory, used as the article cover
← Back to blog

DEEP DIVE

MCP, OAuth, and the Agent Permissioning Problem

The 2026-07-28 MCP revision makes servers strict OAuth 2.1 resource servers with audience-bound tokens and 403 scope challenges. It still does not say which agent is acting for which user.

•May 11, 2026•Updated Sep 29, 2026•15 min
MCPOAuthIdentity

TL;DR

MCP's authorization story is no longer vague. The 2026-07-28 revision makes an MCP server a strict OAuth 2.1 resource server: tokens audience-bound through RFC 8707, discovery through RFC 9728, issuer checks from RFC 9207, and a 403 insufficient_scope challenge for stepping up. What it still does not answer is which agent, acting for which user, may take a given action.

The spec is explicit about that boundary. An MCP server "MUST validate that access tokens were issued specifically for them as the intended audience", and it "MUST NOT pass through the token it received from the MCP client". Both rules govern where a token may be spent. Neither says who is behind the call.

The common reading is that this is an oversight the next revision will close. We don't think it will. Dynamic Client Registration was deprecated in this revision in favour of Client ID Metadata Documents, which identify the client software by URL. That is a statement about which program is calling, not about the person it is working for.

None of this is a complaint about MCP. Audience binding and per-operation scope challenges remove real attacks, and the confused-deputy write-up in the security guidance is sharper than most vendor threat models. The point is narrower: those are protocol mechanics, and one specific action is not a protocol question.

Today: return scope in every WWW-Authenticate challenge, keep scopes_supported to the minimum your server needs, and put the user-and-agent decision in one enforcement point instead of in each server.

Overview

Take a procurement agent that reconciles supplier statements against purchase orders. It reaches three MCP servers: one in front of the ERP, one in front of a document store, and one a teammate wrote in an afternoon to query a warehouse. All three implement the current protocol revision correctly. All three validate token audiences. And nothing in that system can answer whether this agent, working for this buyer, should be allowed to issue a credit note at 02:00.

The interesting thing is how much smaller that gap has become. MCP's authorization chapter used to be one page of advice; the 2026-07-28 revision cites ten specifications, requires RFC 9728 Protected Resource Metadata on the server, requires the RFC 8707 resource parameter on the client, adds RFC 9207 issuer validation to the authorization response, and deprecates Dynamic Client Registration in favour of Client ID Metadata Documents. If you last looked at this in 2025, we would assume most of what you remember has changed.

The general case for why OAuth alone does not cover autonomous agents is covered separately. This post is narrower and more mechanical: what the current MCP revision genuinely settles, what it deliberately leaves to the implementer, and which of those leftovers show up as production incidents rather than as design debates.

MCP and OAuth are protocols. Permissioning is a decision. The protocols now do a good job of making sure a token is well-formed, correctly addressed, and hard to steal. None of that is the same as deciding that a particular action should happen.

What the Spec Actually Requires

An MCP server over HTTP "acts as an OAuth 2.1 resource server", and the client acts as an OAuth 2.1 client. That framing does most of the work, because it pulls in a pile of requirements that are easy to skip when you are writing auth by hand.

ConcernWhat the revision requiresConsequence if you skip it
Finding the authorization serverServers MUST implement RFC 9728 and return WWW-Authenticate with a resource_metadata URL on a 401Clients cannot connect without out-of-band configuration
Naming the intended recipientClients MUST send the RFC 8707 resource parameter in authorization and token requests, "regardless of whether authorization servers support it"Tokens stay general-purpose and replayable across services
Audience checkingServers MUST validate that tokens "were issued specifically for them as the intended audience"Any token from the same issuer works on your server
Not laundering tokensThe MCP server "MUST NOT pass through the token it received from the MCP client"Confused deputy, and downstream logs that name the wrong caller
Identifying the clientPre-registration first where it exists, then Client ID Metadata Documents; Dynamic Client Registration deprecated in 2026-07-28Registration records pile up and cannot be attributed
Mix-up defenceClients record the issuer and validate the iss authorization-response parameter per RFC 9207An attacker-controlled authorization server can swap itself in
State handlesMCP is stateless, and servers "MUST NOT treat possession of a state handle as authentication"A guessed cart or workflow ID reads another user's state

Table 1 — The requirements the current revision pins down, and what breaks without each.

Two of those rows are worth sitting with. The resource parameter is RFC 8707, which exists because "scope is typically about what access is being requested rather than where that access will be redeemed". MCP requires clients to send it whether or not the authorization server understands it, which is a bet on adoption: send the parameter now, get audience-restricted tokens later when the server catches up. The canonical form is the server's own URI, so https://mcp.example.com/mcp is valid and mcp.example.com is not.

The registration row is the bigger change. Dynamic Client Registration, RFC 7591, was how every 2025-era MCP client got a client_id from a server it had never met. It is now listed in the deprecated-features registry with a migration path to Client ID Metadata Documents and an earliest removal of "first revision released on or after 2027-07-28". Under the replacement draft, the client_id is an HTTPS URL that "refers to a document containing the necessary client metadata", and the authorization server fetches and validates it, including checking that the document's own client_id matches the URL exactly. The practical effect is that client identity becomes portable and inspectable instead of being a row in somebody's registration table.

The Scope Challenge Loop

The part of the revision that changes day-to-day code is the error contract. Three status codes, and they mean different things.

StatusWhenWhat the client should do
400Malformed authorization requestFix the request. Not retryable
401No token, or the token is invalid or expiredRead resource_metadata from WWW-Authenticate, discover, authorize
403Token is valid but lacks the scopes for this operationUnion the challenged scopes with what you already hold, re-authorize, retry

Table 2 — The MCP authorization error contract, and the correct client response to each.

The 403 is the one people get wrong, and RFC 6750 is unambiguous about it: insufficient_scope means "the request requires higher privileges than provided by the access token", and it maps to 403, not 401. MCP builds a step-up flow on top. Servers SHOULD include a scope parameter in the challenge, and clients MUST treat the challenged scopes "as authoritative for the current operation".

http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="files:write",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         error_description="File write permission required for this operation"

Two details in that flow save real outages. First, the client computes the union of what it already requested and what the challenge asks for, because a server emitting per-operation challenges will otherwise walk the client down to a single narrow scope and break every other tool it had working. Second, servers SHOULD emit every scope needed for the current operation in one challenge. The spec says why: challenging incrementally "forces multiple authorization round-trips for a single operation and degrades user experience". A user clicking through four consent screens to send one message is a bug in your challenge construction, not in their patience.

The complement is on the server side. scopes_supported is meant to be "the minimal set of scopes necessary for basic functionality", with everything else requested incrementally. The security guidance is blunter about the failure this prevents: publishing every scope you own leads to "expanded blast radius", "audit noise: single omnibus scope masks user intent per operation", and "consent abandonment: users decline dialogs listing excessive scopes". Wildcard and omnibus scopes such as *, all and full-access are named as common mistakes.

A scope challenge is a per-operation question with a per-grant answer. The server can compute the required scopes from the specific request arguments, which is genuinely fine-grained. What comes back is a token carrying that scope for every future call, and nothing in the protocol narrows it again.

What Is Left Open

Here is our honest accounting of what none of the above decides.

Who the agent is. A validated token tells the server two things: a subject, and a client. The client is the software, identified in the new model by a metadata-document URL. Neither field distinguishes an autonomous agent running unattended from the same user's IDE making the same call, and the spec does not ask them to. If your audit record needs to say "the reconciliation agent, acting for this buyer", the protocol will not supply the first half.

Whether this specific action is allowed. Scope is the only vocabulary on the wire, and the guidance itself lists "treating claimed scopes in token as sufficient without server-side authorization logic" as a common mistake. Scopes describe classes of operation. files:write does not distinguish a draft folder from a signed contract, and the server is left to decide.

Who holds the upstream credential. Because token passthrough is forbidden, an MCP server that talks to a real SaaS API "may act as an OAuth client to them", and "the access token used at the upstream API is a separate token". That is correct, and it quietly assigns every MCP server its own credential store, its own refresh logic, and its own disconnect semantics. Ten servers, ten vaults.

How fast revocation lands. The protocol has no revocation primitive at all, so the answer is whatever the identity provider gives you. Microsoft's revokeSignInSessions invalidates all of a user's refresh tokens, and the documentation warns that "there might be a small delay of a few minutes before tokens are revoked". A few minutes is a long time for something making calls in a loop.

The security guidance is at its best on what happens when these leftovers are handled badly. The confused-deputy attack needs four conditions together: a proxy using a static client ID upstream, MCP clients registering dynamically, a third-party authorization server that sets a consent cookie, and no per-client consent at the proxy. Then a malicious link reuses the victim's consent cookie, the consent screen is skipped, and the authorization code lands on the attacker's redirect_uri. The mitigation is a registry of approved client_id values per user, checked before the upstream flow, with the state cookie set only after consent is approved.

Agent

one enforcement point

resolve: which agent,
acting for which user

intersect: user scopes
and agent scopes

attach upstream credential
server-side, per user

MCP server A

MCP server B

one audit record

one enforcement point -> resolve which agent, acting for which user -> intersect user scopes AND agent scopes -> attach the upstream credential server-side, per user -> forward to MCP server A or B -> both write to one audit record -->

Figure 1 — The leftovers collected in one place. The protocol work still happens per server; the identity, intersection, and audit decisions stop being duplicated.

Carrying the User Into the Call

There is a standards answer taking shape for the identity half, and it is worth knowing before building your own.

RFC 8693 draws the distinction the problem needs. Under impersonation, a token makes A indistinguishable from B, so that "insofar as any entity receiving such a token is concerned, they are actually dealing with B". Under delegation, both parties stay visible: "A is an agent for B". The wire form is the act claim, which "provides a means within a JWT to express that delegation has occurred and identify the acting party to whom authority has been delegated", plus may_act, which "makes a statement that one party is authorized to become the actor and act on behalf of another party". Two identities in one token, and a claim saying the pairing was allowed in advance.

MCP's own extension repository takes a different route to the same place. The stable Enterprise-Managed Authorization extension has the user sign in at the enterprise identity provider over OpenID Connect or SAML, exchanges that assertion for an Identity Assertion JWT Authorization Grant, and requires that issued access tokens "MUST be audience-restricted to the MCP Server identified by the resource claim". User identity rides in the grant's sub and scope claims; the client identifies itself with a client_id that may be a metadata-document URL. The reason it lives in an extension rather than the core is instructive: extensions are optional and additive by design, so an enterprise can have this while the base protocol stays simple.

MCP serverResource authz serverAgent (MCP client)User at IdPMCP serverResource authz serverAgent (MCP client)User at IdPSSO assertion (OIDC/SAML)exchange assertion for grantaccess token, audience = MCP servertools/call + Authorization: Bearervalidate audience, then authorize the actionresult, or 403 + scope challenge

Figure 2 — The enterprise-managed path. The audience is pinned by the grant; the action decision is still the server's own.

Neither mechanism decides anything for you. They are both plumbing for carrying two identities to the place where a decision gets made. That place is the thing you have to build or buy.

Failure Modes

These are the ones we would instrument, because each looks like a different bug than it is.

SymptomLikely causeHow to tell
Client loops 401 foreverresource_metadata not returned on the 401, or the client is not parsing WWW-AuthenticateCurl the endpoint without a token and read the header
Consent screen appears four times for one taskServer challenges one scope at a time instead of emitting all required scopes togetherCount distinct scope values across consecutive 403s
A tool that worked yesterday now 403sClient re-authorized with only the challenged scope and dropped previously granted onesCompare the token's scope before and after the last step-up
Token accepted by the wrong serverNo audience validation; any token from the same issuer passesPresent a token minted for a different resource and see if it works
Upstream logs name the MCP server, not the agentToken passthrough, or the upstream credential is shared across all usersCheck whether the upstream call uses a per-user credential
Access continues after offboardingRevocation latency at the identity provider plus long-lived upstream tokensTime the gap between revocation and the first rejected call

Table 3 — Six MCP authorization failures and the cheapest signal that identifies each.

The offboarding row is the one that turns into an audit finding. Nothing in MCP revokes anything, and the identity provider's revocation only reaches tokens that provider issued. Upstream credentials held by individual MCP servers are outside that blast radius entirely, which is why the count of places credentials live matters more than any single one of them being well built.

Every MCP server you add is another copy of the same four decisions. Identity resolution, action authorization, credential custody, and audit. The protocol makes the wire format identical across servers. It does nothing to stop those four from diverging.

Where Fabriq Fits

Agentic Fabriq holds credentials per user in a vault and attaches them server-side at the moment of the call, so the agent's reasoning loop never handles the secret. Every call carries two identities, the agent and the acting user, and produces an audit record naming both. An agent's effective tool list is the intersection of the user's scopes and the agent's own, which is the concrete version of the "user-and-agent" check this post keeps pointing at.

What it does not do is reach into a provider on your behalf. Clearing a vault copy of a credential is not the same act as invalidating a grant upstream, so the call to the identity provider's revocation endpoint stays yours to make, and the latency above is still the latency you get.

Frequently Asked Questions

Does MCP support OAuth? Yes, and specifically OAuth 2.1 over HTTP transports. Authorization is OPTIONAL in the spec, but when it is implemented an MCP server is an OAuth 2.1 resource server and the client is an OAuth 2.1 client. STDIO implementations are told not to follow the authorization spec at all and to "retrieve credentials from the environment" instead.

What is the difference between 401 and 403 in MCP? A 401 means authorization is required or the token is invalid, and the client restarts discovery from the WWW-Authenticate header. A 403 means the token is fine but lacks scope, carries error="insufficient_scope", and should trigger a step-up rather than a fresh discovery round.

Do I still need Dynamic Client Registration? Not for new work. It is deprecated as of 2026-07-28, with Client ID Metadata Documents as the migration path, and it remains in the spec for at least twelve months under the deprecation policy. Keep supporting it as a fallback for authorization servers that advertise registration_endpoint but not client_id_metadata_document_supported.

Can an MCP server forward my token to the upstream API? No. "MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server", and the token used upstream must be a separate one the server obtained as an OAuth client in its own right.

What is the resource parameter for? It names the server the token is meant for, so the authorization server can audience-restrict it. RFC 8707 defines invalid_target for the case where "the requested resource is invalid, missing, unknown, or malformed". MCP clients must send it even when the authorization server ignores it.

Is a session or state handle enough to authenticate? No. MCP is stateless and has no protocol-level sessions. Where a server mints a handle for a cart or a workflow, servers "MUST NOT treat possession of a state handle as authentication", and the handle should be bound server-side to the verified user, keyed as <user_id>:<handle>.

Which scopes should my MCP server publish? The minimum set needed for basic functionality, with everything else requested through step-up challenges. Publishing the full catalogue in scopes_supported is listed as a common mistake, alongside wildcard scopes and bundling unrelated privileges "to preempt future prompts".

How do I know which user an agent's tool call was for? Not from the protocol. Either carry it in the token, through RFC 8693 delegation semantics or the Enterprise-Managed Authorization extension's identity grant, or resolve it at an enforcement point in front of the servers. What you cannot do is infer it from a client_id, which names the software.

Conclusion

The version of this argument we would have written a year ago was that MCP punts on authorization. That is no longer true, and pretending otherwise misleads people into rebuilding things the spec now handles: audience binding, discovery, issuer validation, and a step-up flow with real error semantics. Implement those properly and a large class of attack goes away.

What remains is not a protocol gap so much as a category difference. Audience binding answers where a token may be spent. Scope answers what class of operation it covers. Neither answers whether this agent, working for this person, should do this thing now, and both answers you might want are per-call judgements rather than properties of a credential.

So the practical advice is unglamorous. Follow the spec exactly, including the parts that feel like paperwork, because iss validation and audience checks are cheap and the attacks they stop are not. Then decide where the per-call judgement happens, once, and make every MCP server you connect route through it. Doing that late is how organisations end up with a dozen servers, a dozen credential stores, and no single answer to what the agent did.

A protocol tells you the call is well-formed and correctly addressed. It was never going to tell you whether the call should happen. Build the place that decides before the first incident makes the case for you.

Sources

All URLs read 2026-09-29.