sauble.ai

Technical article · MCP · OAuth 2.1

MCP's new authorization model: your client is a URL, your session is gone

The MCP 2026-07-28 release candidate (final on July 28) locks in a quiet redesign of how MCP clients authenticate to MCP servers they've never met. If you're building an MCP client, an MCP server, or the SaaS behind one, several patterns you may have already shipped are now structurally dead — and a few new obligations arrived with no fanfare. Here's what actually changed, why, and what to do about it.

The threat model OAuth never had

Classic OAuth deployments have a comfortable shape: one application talks to one authorization server it was configured for, by a developer, in advance. Every trust relationship is established before the first request.

MCP breaks every clause of that sentence. A general-purpose client — an agent, an IDE, a chat app — connects to servers its developers have never heard of, at runtime, on a user's say-so. Some of those servers will be misconfigured. Some will be malicious. The client can't pre-register anywhere, the server can't vet clients in advance, and the same client juggles tokens for many servers and many authorization servers simultaneously. That last part matters: entire attack classes that were theoretical in classic OAuth — token replay across services, authorization-server mix-up, the confused deputy — are the default weather in MCP's topology.

The 2026-07-28 release candidate is where the spec finishes reorganizing itself around that reality. It isn't a new protocol — it's a profile of OAuth 2.1 (still an IETF draft, though one that largely consolidates already-stable practice) with a small set of RFCs promoted from "good idea" to "mandatory," plus a transport change that has bigger authorization consequences than most of the coverage acknowledges. This article assumes you know roughly how OAuth works; if you want the mechanics from first principles, we've published a set of interactive primers (browser flows, headless flows, MCP authorization end-to-end) that this piece builds on.

Five changes deserve your attention. Two of them will break running code.

1 · The session is gone. Per-request auth is all that's left.

The RC removes the initialize/initialized handshake (SEP-2575) and the Mcp-Session-Id header along with protocol-level sessions (SEP-2567). Protocol version, client info, and capabilities now travel in namespaced _meta fields (io.modelcontextprotocol/*) on every request, and new HTTP headers are required so gateways and rate-limiters can route on the operation without parsing bodies (SEP-2243): Mcp-Method on every request, plus Mcp-Name where the operation targets a named thing (tools/call, resources/read, prompts/get). Servers reject requests where headers and body disagree.

This is usually described as a scaling win, and it is — any request can land on any server instance, no sticky routing, no shared session store. But read it as an authorization change and it has teeth:

The dead pattern: validate the bearer token once at initialize, stash the auth context in session state, and trust the session for every subsequent call. A large fraction of early MCP servers — including SDK examples people copied — work exactly this way. Under the RC there is no initialize to hook and no session to stash into. Every request must be independently authenticated and authorized. If your server caches "who is this" anywhere except the token itself, you have a migration item.

The flip side is a genuine security upgrade you get for free: there is no session state to hijack, fixate, or leak across tenants. The bearer token — short-lived, audience-bound, carried in the Authorization header on every request, never in a URL — is the entire security context. Design your server so that a request plus its token is a complete, self-contained authorization question, and statelessness stops being a migration burden and starts being the simplest thing that works.

Here's first contact under the new rules, condensed — an MCP client meeting a protected server it has never seen (step through; dashed arrows ride the browser, solid are server-to-server):

2 · Your client is now a URL

The second breaking-ish change is quieter: Dynamic Client Registration is deprecated in MCP's authorization profile (RFC 7591 itself lives on outside MCP, and DCR remains available as a compatibility fallback). Its replacement, Client ID Metadata Documents (CIMD), changes what a client is: the client_id becomes an HTTPS URL pointing at a JSON document the client's vendor hosts — name, redirect URIs, grant types. When an authorization server sees a URL-shaped client_id, it fetches the document, validates it, and caches it. Registration collapses into dereferencing.

Why? DCR solved the stranger problem by letting anyone register — which meant authorization servers accumulated unbounded tables of anonymous clients with no trust signal and no cleanup story. Every AS operator who shipped DCR for MCP discovered they were running an open write endpoint to their client database. CIMD inverts the burden: the client's identity lives at a domain the client's vendor controls and demonstrably owns, the AS stores nothing until it has something worth caching, and one identity works across every AS (a CIMD client_id is portable; DCR credentials never were).

The details that will bite you:

3 · Tokens with a blast radius of one

Three mandates work together to make any single stolen or misused token nearly worthless. None is novel cryptography; all are old RFCs promoted to MUST because MCP's topology makes their absence exploitable.

Resource indicators (RFC 8707). Clients must send resource=<canonical server URI> in both the authorization and token requests (RFC 8707 asks the authorization server to then audience-restrict the token); MCP goes further and requires the server to reject any token whose audience isn't itself (the resource-server check itself is OAuth 2.1 §5.2; and when the access token is a JWT, RFC 9068 §4 makes that rejection an explicit MUST). This is the confused-deputy defense: without it, a malicious MCP server that receives your token can replay it against every other MCP server you use. With it, a token for server X is inert everywhere else. The operational catch: many authorization servers silently ignore resource and mint audience-less tokens. The client did everything right; the token is still promiscuous. If you're choosing or operating the AS, audience-binding support is a hard requirement, not a nice-to-have — and if you're building a server, reject tokens whose aud isn't exactly you, even if they validate otherwise.

No token passthrough. An MCP server must accept only tokens minted for it, and must never forward the client's token to upstream APIs. If your server needs to call downstream services on the user's behalf, mint a new credential for that hop (token exchange, RFC 8693, is the standard shape). Passing the inbound token upstream feels convenient and works in demos; it also makes your server a token-laundering device and destroys the audit chain. This is the rule we see violated most often in the wild.

Issuer validation (RFC 9207). Clients must record which authorization server they expect before opening the browser, then verify the iss parameter on the callback — byte-for-byte, no URL normalization — whenever it's present. (There's an asymmetry worth knowing: the client's validation is a MUST, but the AS's inclusion of iss is still only a SHOULD today, one the spec expects to tighten to MUST — so validate it whenever the AS sends or advertises it.) This kills mix-up attacks, where one authorization server (or one rogue MCP server's metadata) answers a flow another one started. In classic OAuth, mix-up was exotic because clients talked to one AS. An MCP client talks to many, concurrently, some discovered five seconds ago — which is why the RC hardens this (SEP-2468) and why the validation applies even to error responses.

4 · 403 is an API now

The spec's scope-challenge design deserves more attention than it gets, because it turns authorization failures from dead ends into a protocol. When a request needs a scope the token doesn't carry, the server should respond with 403 and a WWW-Authenticate challenge naming the scope(s) the operation requires (the spec makes this a SHOULD, and lets a server include related scopes beyond the bare minimum to save round-trips):

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
  scope="config:write",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

The client then runs step-up authorization: re-authorize requesting the union of its previously granted scopes plus the challenged ones (union, not just the new scope — otherwise you trade away permissions you already had), retry a bounded number of times, and treat repeated failure as permanent.

Two consequences worth designing for:

5 · Scopes are not entitlements (the multi-tenant section)

Here is the part the spec doesn't cover at all, and the first thing every SaaS vendor hits in implementation: OAuth scopes and your product's permission system are different layers, and conflating them produces security bugs in both directions.

Think of two independent gates. Gate 1 — entitlements: what the customer's admin enabled for this user in your tenant model (role, plan, product tier). Lives in your database, decided long before any token existed. Gate 2 — scopes: what this particular client session was granted — the intersection of what the client requested and what Gate 1 allows. Lives in the token. Effective permission is the intersection of both, and the ordering matters: user consent operates strictly inside the box the admin drew. No amount of clicking "Approve" on a consent screen can mint a scope the tenant's entitlements don't back — and if your authorization server's consent flow doesn't consult entitlements at mint time, you've built exactly that bug.

The reverse bug is subtler: treating the token's scopes as the only check and skipping your runtime entitlement enforcement. Entitlements change mid-token-lifetime — plans downgrade, admins revoke roles, users get offboarded. A one-hour token minted under yesterday's entitlements is a one-hour window of unauthorized access unless your resource server keeps enforcing product-level checks per request. Scopes are a projection of entitlements at mint time, not a replacement for checking them.

Watch both gates operate — a permitted call, a denied one where step-up runs and the admin's decision correctly overrules the user's consent, and the silent refresh that keeps sessions alive across a workday:

Design rule that falls out: keep one system of record for permissions (your existing RBAC/entitlements), and make scopes a materialized projection of it at token-mint time — never a second, parallel permission system that can drift. Your MCP scope table should be derivable, mechanically, from the permission model you already have.

6 · What's still yours to design

Equally important is what the spec deliberately leaves out. Knowing the boundary saves you from waiting for guidance that isn't coming:

7 · The migration checklist

ChangeIf you build a clientIf you build a serverIf you run the AS
Sessions removedSend _meta + Mcp-Method/Mcp-Name on every request; drop initialize-based lifecyclesValidate the token on every request; delete session-cached auth context; reject header/body mismatches
CIMD replaces DCRHost a stable HTTPS metadata document; implement the pre-reg → CIMD → DCR priority; declare application_type in any DCR fallbackAdvertise + implement CIMD (fetch, validate, cache); define a client-domain trust policy; keep DCR only as a consciously legacy path
Resource indicatorsSend resource= on authorize and token requests, alwaysReject any token whose aud isn't exactly your canonical URIHonor resource and mint audience-bound tokens — silently ignoring it undermines everyone downstream
Issuer validationRecord the expected issuer pre-redirect; verify iss byte-for-byte on callbacks, including error responsesInclude iss on authorization responses; advertise it in metadata
Scope challengesImplement step-up with scope union + retry limitsEmit complete, consistent scope challenges; advertise baseline scopes on 401Support incremental consent without re-prompting for already-granted scopes
No passthroughNever send a token anywhere except the server it was minted forNever forward inbound tokens upstream; use token exchange for downstream hopsConsider RFC 8693 support — your vendors will need it
Scopes vs entitlementsKeep enforcing product entitlements per request; scopes don't replace themConsult entitlements at mint time; consent must not expand beyond admin grants

The two items that genuinely break running code are the session removal (architectural, if you cached auth state) and DCR deprecation (operational, on the AS side). Everything else is tightening.

One summary you'll hear elsewhere: "MCP now just uses OAuth, nothing to see." The protocol is standard; the deployment topology isn't, and the topology is where the work lives. One client, many servers, mutual strangers, some hostile — every mandate above exists because someone worked out how that topology gets exploited. Prepare for the topology, not just the spec.

Going deeper