Part 2 of a two-part series on MCP security. Part 1 attacked the server itself — sandbox escapes, command injection, and the supply chain. This part assumes all of that is patched. We break in through the identity layer instead.
Background
Part 1 was about code. An attacker who wins there wins because a startswith() check was wrong, or a trust dialog fired after the payload ran (or didn’t at all), or a description field got fed to a model as an instruction. Those are fixable.
The identity layer is different. It is not a bug you close. It is a set of decisions about who gets to ask the server for what, and those decisions are spread across different parties: the MCP client, the MCP host, the resource server (RS), the authorization server (AS), and the enterprise IdP. When the answer is wrong, no code is broken. The server does exactly what it was told. It might have been told by the wrong person.
MCP spent 2025 and 2026 fixing this. The 2026-07-28 specification shipped on July 28 and it closes a genuine list of attacks. The Enterprise-Managed Authorization (EMA) extension went stable on June 18, 2026 and moves consent off the user and into the IdP.
This post covers three things:
- The mechanism. What the MCP authorization spec actually says, which RFCs it pulls in, and why each one is there.
- Consent. One of the most misunderstood words in this whole stack, the confused-deputy attack that comes from getting it wrong, and how ID-JAG fixes it.
- What is left. After July 28, and after EMA. Where the doors still don’t close, and what that means for detection.
The shape of the problem
The moment the MCP server moved from being local to a remote HTTP endpoint, everything changed. Now there is a network, multiple users, and a server that has to decide for each request whether the caller is allowed to do the thing.
OAuth has always had two distinct roles:
- Authorization Server (AS) — the token factory. It knows who the user is, decides what to grant, and mints access tokens.
- Resource Server (RS) — the API. It validates tokens and serves data.
Early MCP diagrams showed the MCP server doing both. That is allowed, but the spec was forcing it, because of how discovery worked. The client was told to fetch RFC 8414 authorization server metadata from the MCP server’s base URL, which meant the MCP server had to be an authorization server, and that is a bad requirement.
The current spec separates the roles cleanly:
- The MCP server is an OAuth 2.1 resource server. Its entire job is: validate the token, check the audience, enforce authorization, serve the response.
- The MCP client is an OAuth 2.1 client.
- The authorization server is explicitly out of scope. It can be co-hosted, or it can be any IdP. The spec does not care.
The RFC stack
| Spec | Purpose | Key rule | Stops |
|---|---|---|---|
OAuth 2.1 (draft-ietf-oauth-v2-1-13) | Consolidates OAuth 2.0 + BCPs | PKCE mandatory, all clients | Code interception |
| RFC 9728 — Protected Resource Metadata | RS points at its AS without being one | MCP servers MUST implement; clients MUST use for discovery | Forces the RS/AS role split |
| RFC 8414 / OIDC Discovery | AS publishes its endpoints | Client learns authorization_endpoint, token_endpoint, issuer | Hardcoded/stale endpoints |
| RFC 8707 — Resource Indicators | Binds a token to one server | resource param MUST be sent and MUST be validated as audience; servers MUST NOT accept or transit other tokens | Cross-server token replay |
| RFC 6750 — Bearer Token Usage | Tells the client what it’s missing | 401 + scope = no token; 403 + insufficient_scope = wrong scope | Blind scope guessing |
| RFC 7591 → CIMD | Client identity without pre-registration | client_id = a URL the client controls (draft-ietf-oauth-client-id-metadata-document-00), replacing unauthenticated DCR /register | Unattributable, unauthenticated registration |
| RFC 8693 + RFC 7523 → ID-JAG | Moves consent from browser to IdP | Token Exchange (8693) mints an ID-JAG; JWT-bearer grant (7523) redeems it at the Resource AS | Per-app consent screens |
The flow, end to end
1. Unauthenticated call, 401.
POST /mcp HTTP/1.1
Host: mcp.example.com
MCP-Protocol-Version: 2026-07-28
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="crm:read"
2. Protected Resource Metadata (RFC 9728). Client fetches /.well-known/oauth-protected-resource, reads authorization_servers[], picks one.
3. AS Metadata (RFC 8414 / OIDC Discovery). Client fetches authorization_endpoint, token_endpoint, scopes_supported, and the canonical issuer. Records the issuer (RFC 9207).
The discovery phase — the client learns where to authorize before it ever sees a token.
4. Client identity. CIMD (client_id = an HTTPS URL the client controls) or DCR (RFC 7591, deprecated) if the AS doesn’t support CIMD.
5. Authorization request. PKCE code_challenge (OAuth 2.1, mandatory), state, resource (RFC 8707 — binds the eventual token to this MCP server, blocks cross-server replay), and scope.
6. Callback. iss validated against the issuer recorded in step 3.
7. Token exchange. code + code_verifier + resource ⇒ access token.
8. Authenticated call, and the server validates. Signature, expiry, issuer, and audience — that the token was minted for this server, not replayed from another. Only then does it check scope against the requested tool call.
The full authorization code flow, from the first unauthenticated call through to a standard MCP message exchange.
Consent
“User logs in” and “user authorizes” are different steps. OAuth says nothing about the first. The second is the consent screen — and in an enterprise it was asking the wrong person.
The confused deputy
Before we get to fixing consent, here is what happens when you get it wrong.
Setup. An MCP proxy sits between MCP clients and a third-party API. To MCP clients it’s an authorization server. To the third-party AS it’s a single OAuth client with one static client_id. Two OAuth layers stacked on one box.
Preconditions — all four must hold:
- The proxy uses a static
client_idwith the third-party AS. - The proxy lets MCP clients dynamically register, each getting its own
client_id. - The third-party AS sets a consent cookie after the first authorization.
- The proxy does not implement per-client consent before forwarding upstream.
The attack. The user’s one legitimate authorization plants a consent cookie bound to the proxy’s static client_id. The attacker then registers its own client at the proxy, sends the user a link, and rides that cookie through the AS — which sees the same static ID it already trusts and skips consent entirely. The proxy hands the resulting code to whatever redirect_uri the attacker registered.
The confused deputy: one legitimate consent, then a cookie the attacker rides straight to the user’s access token.
The problem
Even a perfect consent screen is wrong at work.
A user logs in to Claude via SSO. Then, to connect Google Drive, they get redirected to Google, which redirects to the IdP to authenticate again, which redirects back to Google for a consent prompt, which redirects back to Claude. Say there are ten servers — that means ten SSO round-trips and ten prompts before anyone does anything.
Okta sees you log into Asana. It never sees that ChatGPT is now connected to Asana on your behalf. That is shadow IT with a corporate token attached.
EMA: moving consent to the IdP (RFC 8693 + RFC 7523)
The Enterprise-Managed Authorization extension (io.modelcontextprotocol/enterprise-managed-authorization, from SEP-990) went stable on June 18, 2026.
The idea: delete the consent redirect. The IdP already knows who you are and what you’re allowed to touch.
EMA in one picture: the IdP makes the decision, so there is no consent screen to phish.
The ID-JAG is a signed, 300-second delegation minted by the IdP after a policy check, then redeemed at the Resource AS as a JWT-bearer grant (RFC 7523) for an access token audience-restricted to the MCP server. No consent screen — an IdP decision.
What the 2026-07-28 spec closes
The RC locked May 21, 2026. The final specification shipped July 28, 2026, with a 12-month deprecation window on legacy versions.
Statelessness kills auth bugs. The initialize handshake (SEP-2575) and the Mcp-Session-Id header (SEP-2567) are gone, so protocol-level session hijacking and the shared-queue prompt-injection variant have no primitive left. Server-initiated requests may now only fire while processing a client request (SEP-2260) — no prompts out of nowhere. Six auth SEPs harden the rest: SEP-2468 (mandatory iss validation), SEP-837 (application_type in DCR), SEP-2352 (credentials bound to issuer), SEP-2207 (refresh tokens), SEP-2350 (scope accumulation), SEP-2351 (.well-known suffix).
What’s still open
- The IdP sees issuance, not use. IdP visibility “does not extend to the actual MCP traffic between the MCP Client and Server.” It knows a
chat.readtoken was minted at 14:02. Not that by 14:03 it had dumped every channel. - The 300s grant becomes a 24-hour token. The ID-JAG in the spec’s own example expires in 300 seconds. The access token it produces:
expires_in: 86400. Revoke in Okta at 14:05 and the 14:02 token works until tomorrow. - Cross-agent has no chain. ID-JAG’s
audis pinned to a single Resource AS — single-hop by construction. When agent A calls agent B, the grant stops at A. No attenuation exists across the hop, and no provenance survives it. The token says “user U”, not “user U, three hops back, via an agent that read a poisoned tool description.” Every check passes, since the call is authorized. - The shadow-MCP case. EMA governs servers the admin enabled. It has nothing to say about the over-scoped tokens, or the ones nobody registered — the stdio server a developer added to their
settings.jsonthis morning with a PAT in an env var. That server never touches an authorization server, never mints an ID-JAG, never appears in an IdP log.
Detection and prevention with SlashID
Every gap above has the same fingerprint: the token was valid. Signature checked, audience matched, scopes present, IdP policy said yes five minutes ago. Nothing in the OAuth stack fired, because at the protocol layer nothing was wrong.
So the question isn’t “was this authorized.” It was. The question is: given everything else we know about this identity, does this behavior make sense? — which a token validator can’t answer and a graph can. SlashID sits at that layer.
Mapping the gaps to SlashID controls
| # | Gap | Attacker/failure action | SlashID control |
|---|---|---|---|
| 01 | IdP sees issuance, not use | A valid chat.read token minted at 14:02 fans out across the workspace at 14:03. No OAuth signal — the grant was legitimate. | Behavioral / NHI detection on the post-issuance read burst — high tool read fan-out seconds after first token use, matching no human or NHI baseline. |
| 02 | 300s grant → 24h token | Resource AS validates against its own rules, not the IdP. Revoke in Okta at 14:05; the 14:02 token lives until tomorrow. | ISPM posture finding for token lifetime exceeding the ID-JAG that authorized it, plus short-lifetime / sign-in-frequency enforcement; Identity Graph flags the stale grant whose upstream identity was revoked or drifted out of policy. |
| 03 | Cross-agent has no chain | ID-JAG’s aud is single-hop. Agent A→B, the grant stops at A. The token at C says “user U”, not “via an agent that read a poisoned description two hops back”. | Identity Graph traversal reconstructs the path the aud claim can’t carry (C→B→A→human→tool response). |
| 04 | Shadow MCP server | The stdio server in settings.json with a PAT in an env var never touches an AS, never mints an ID-JAG, never hits an IdP log. | Identity Graph inventory surfaces the unsanctioned MCP server, OAuth app, and at-rest credential as graph nodes the moment they exist — the same shadow-SaaS discovery pattern from the PhaaS analysis. |
Conclusion
The 2026-07-28 specification is a serious upgrade. Statelessness removes an entire class of session-based attacks, the auth hardening closes real protocol gaps, and EMA finally gives enterprises a consent model that makes sense. None of that was true a year ago.
But the spec governs the authorization, not what happens after it. The four gaps above persist because they sit outside the protocol’s scope: post-issuance behavior, token lifetime drift, multi-hop provenance, and shadow infrastructure that never touches an authorization server in the first place. Closing them requires identity-layer detection that operates on what the token actually does, not just whether it was correctly issued. SlashID’s Access Graph and behavioral detections sit at that layer, covering each of the four gaps mapped above.
Bring your OAuth and MCP server inventory
We'll show you where the four gaps above land in your environment — the tokens outliving the grants that authorized them, the agent hops that lose provenance, and the servers nobody registered.
References
[1] Model Context Protocol, “Authorization,” Specification (2026-07-28). https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
[2] A. Parecki, “Client Registration and Enterprise Management in the November 2025 MCP Authorization Spec,” November 25, 2025. https://aaronparecki.com/2025/11/25/1/mcp-authorization-spec-update
[3] IETF OAuth WG, “Identity Assertion JWT Authorization Grant,” draft-ietf-oauth-identity-assertion-authz-grant. https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/
[4] Model Context Protocol, “Enterprise-Managed Authorization,” ext-auth specification (stable). https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx
