MCP authorization in practice: step-up, no passthrough, Entra ID

MCP authorization in practice: step-up, no passthrough, Entra ID

Validating a token is where authorization starts, not where it ends. Once your Model Context Protocol (MCP) server checks tokens, three practical questions follow: how to call another service for the user without handing it the user’s token, what to do when a valid token is missing a scope, and what changes when your authorization server is Microsoft Entra ID. What I find easy to miss is how much of the new work lands on clients and authorization servers, not on your server.

This is part 19 of my series on what an MCP server does under the 2026-07-28 specification, after part 18 on what an MCP server must do as an OAuth resource server. It’s about an HTTP server that uses authorization; a server on standard input and output (stdio) takes its credentials from the environment instead. The facts are as I read them in October 2026.

In brief

  1. Never forward the client’s token. Accept only tokens issued for your server, and get a separate, delegated token for each downstream service you call.
  2. When a valid token lacks a scope, your server SHOULD answer 403 with insufficient_scope, and SHOULD name every scope the operation needs, so the client can step up in one round.
  3. Most of the 2026-07-28 authorization changes land on clients and authorization servers: issuer checks, a mandatory resource parameter, a new registration order with Dynamic Client Registration deprecated, and credentials bound to their issuer.
  4. With Microsoft Entra ID, as of October 2026, expect pre-registered clients, and, on Entra v2, an audience named by scopes on an api:// Uniform Resource Identifier (URI), your application ID URI.
  5. Without sessions, every request carries its own token, so what outlives a request needs a check of its own: requestState bound to the caller, handles and task IDs checked on every call, and (my advice) a policy for subscriptions/listen streams whose token expires.

Never forward the client’s token

Part 18’s fourth and last duty is where this part starts: your server never forwards the client’s token. For any downstream application programming interface (API) it calls, it gets a separate token, for example through an on-behalf-of or token-exchange flow. Here’s the difference:

Forbidden: passthrough: Client, MCP server, Downstream API; Correct: delegated tokens: Client, MCP server, Authorization server, Downstream API; Client → MCP server: token aud = MCP server; MCP server → Downstream API: same token forwarded; Client → MCP server: token aud = MCP server; MCP server → Authorization server: on-behalf-of / token exchange; Authorization server → MCP server: new token aud = downstream; MCP server → Downstream API: downstream tokenForbidden: passthroughCorrect: delegated tokensClientMCP serverDownstream APIClientMCP serverAuthorization serverDownstream APItokenaud = MCP serversame tokenforwardedtokenaud = MCP serveron-behalf-of /token exchangenew tokenaud = downstreamdownstream token

In both flows the client sends the same token, issued for your server. What differs is what your server does next: in the delegated flow, the downstream API only ever sees a token issued for it.

Step-up: a 403 that names every missing scope

Scopes are the other way to keep a token’s reach small. Part 18 kept your scopes_supported to the minimal set; step-up is how a client gets more when an operation needs them.

When a token is valid but lacks the scopes an operation needs, the specification’s Authorization page says the server SHOULD return 403 with error="insufficient_scope", the required scope and resource_metadata:

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"

Three rules go with it:

  • Servers SHOULD list all the scopes the operation needs in a single challenge. Challenging one scope at a time forces repeated authorization round trips.
  • Combining scopes across operations is the client’s job, so your server doesn’t need to echo scopes the client already has.
  • Servers MUST respect scope hierarchies, where a broader scope implies the narrower ones it contains.

The client adds the challenged scopes to the ones it asked for before, authorizes again and retries:

MCP client → MCP server: tools/call write_file token with files:read; MCP server → MCP client: 403 insufficient_scope scope "files:write"; Note (MCP client): Union of previously requested scopes + challenged scopes; MCP client → Authorization server: re-authorize files:read files:write; Authorization server → MCP client: new token; MCP client → MCP server: retry tools/call write_file; MCP server → MCP client: successAuthorization serverMCP serverMCP clientUnion of previouslyrequested scopes+ challenged scopestools/call write_filetoken with files:read403 insufficient_scopescope "files:write"re-authorizefiles:read files:writenew tokenretry tools/call write_filesuccess

Status codes and two smaller rules

StatusUse
401Authorization required, or token invalid or expired
403Insufficient scope or permissions
400Malformed authorization request
  • Don’t advertise offline_access in challenges or in scopes_supported. Refresh tokens aren’t a resource requirement.
  • Follow the specification’s separate security-considerations page too. It covers audience binding, token theft, communication security, code protection, mix-up and confused-deputy attacks, open redirection, and the security of Client ID Metadata Documents.

Clients, authorization servers and Entra ID

Most of the new authorization rules in 2026-07-28 apply to clients and authorization servers, not to your server, but they change how you configure things:

ChangeWhat it means
Issuer validation, Request for Comments (RFC) 9207Authorization servers SHOULD include iss in authorization responses, and clients MUST compare it to the recorded issuer, by exact string comparison, before redeeming the code.
The resource parameter is mandatoryClients send the server’s canonical URI in both the authorization and the token request: lowercase scheme and host, no fragment, ideally no trailing slash.
Registration priorityClient ID Metadata Documents come first, then pre-registration. Dynamic Client Registration is deprecated, and where it’s still used, clients must send an appropriate application_type.
Credentials bound to their issuerClients key stored credentials by issuer, and register again when the authorization server changes.

For your server, the visible change is that every client now sends your canonical URI as resource, the audience your token check from part 18 looks for (Entra ID is the exception). Part 2 says how long a deprecated feature such as Dynamic Client Registration keeps working.

Microsoft Entra ID, as of October 2026

Entra ID is a common enterprise authorization server. Here’s what to expect from it:

  • It doesn’t support open Dynamic Client Registration or Client ID Metadata Documents, so expect clients to be pre-registered.
  • Entra v2 identifies the audience through scopes on your api:// application ID URI, not through the resource parameter of RFC 8707.
  • Validate the token’s aud against your app’s client ID or application ID URI, and make sure the resource in your Protected Resource Metadata lines up with it.

For an organization-wide rollout, also look at the Enterprise-Managed Authorization extension, which a later part covers. Microsoft’s MCP support is evolving quickly, so check the current Entra documentation before you rely on any of this.

Authorization without sessions

With no session, there’s no logged-in connection: every POST carries its token and authenticates on its own. A few rules from earlier parts meet here:

  • requestState binds the authenticated principal, or one user could replay another’s state, as part 14 explains.
  • Handles and task IDs are names, not capabilities. Check the caller’s authorization against them on every call. Part 5 gives the rule for handles, and the specification makes authorizing every task-related request a server MUST.
  • Gateways can enforce per-tool policy from the Mcp-Method and Mcp-Name headers without parsing bodies. If you rely on that, make sure a header that disagrees with the body gets HeaderMismatchError (-32020), as part 7 describes.

The rest is my advice:

  • Cache the JSON Web Key Set (JWKS) and your validation results, so per-request checks stay cheap.
  • Pick a policy for a token that expires while a subscriptions/listen stream is open: close the stream at token expiry, so the client reconnects with a fresh token, or cap the stream’s lifetime at the token’s remaining lifetime. Part 17 covers the streams.
  • If your lists vary by scope, return only the tools, prompts and resources the caller can use, and mark such lists cacheScope: "private" (part 6 covers the caching hints).

Method and caveats

  • Built from my guide’s chapter on authorization (plus one duty from its threat-model chapter), written against the 2026-07-28 specification as I read it in October 2026.
  • The Entra ID notes are as of October 2026.
  • In “Authorization without sessions”, the caching, the stream policy and the private lists are my advice; the other notes restate the specification’s rules.