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

October 23, 2026 · Part 19 of 35

![MCP authorization in practice: step-up, no passthrough, Entra ID](https://pournasserian.com/_astro/lead.D-_eyUPh_1DYmvV.webp)

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](https://pournasserian.com/writing/mcp-2026-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:

```mermaid
flowchart TB
    subgraph bad["Forbidden: passthrough"]
        direction LR
        c1["Client"] -->|token<br/>aud = MCP server| m1["MCP server"]
        m1 -->|same token<br/>forwarded| d1["Downstream API"]
    end
    subgraph good["Correct: delegated tokens"]
        direction LR
        c2["Client"] -->|token<br/>aud = MCP server| m2["MCP server"]
        m2 -->|on-behalf-of /<br/>token exchange| as2["Authorization server"]
        as2 -->|new token<br/>aud = downstream| m2
        m2 -->|downstream token| d2["Downstream API"]
    end
    bad ~~~ good
```

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](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) says the server SHOULD return 403 with `error="insufficient_scope"`, the required `scope` and `resource_metadata`:

```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"
```

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:

```mermaid
sequenceDiagram
    participant C as MCP client
    participant M as MCP server
    participant A as Authorization server
    C->>M: tools/call write_file<br/>token with files:read
    M-->>C: 403 insufficient_scope<br/>scope "files:write"
    Note over C: Union of previously<br/>requested scopes<br/>+ challenged scopes
    C->>A: re-authorize<br/>files:read files:write
    A-->>C: new token
    C->>M: retry tools/call write_file
    M-->>C: success
```

### Status codes and two smaller rules

| Status | Use |
|---|---|
| 401 | Authorization required, or token invalid or expired |
| 403 | Insufficient scope or permissions |
| 400 | Malformed 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:

| Change | What it means |
|---|---|
| Issuer validation, Request for Comments (RFC) 9207 | Authorization 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 mandatory | Clients 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 priority | Client 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 issuer | Clients 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](https://pournasserian.com/writing/mcp-2026-what-changed) 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](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization), 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](https://pournasserian.com/writing/mcp-2026-request-state) explains.
- **Handles and task IDs are names, not capabilities.** Check the caller's authorization against them on every call. [Part 5](https://pournasserian.com/writing/mcp-2026-stateless-requests) 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](https://pournasserian.com/writing/mcp-2026-transports) 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](https://pournasserian.com/writing/mcp-2026-subscriptions-listen) 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](https://pournasserian.com/writing/mcp-2026-caching-and-pagination) 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.

I'm [Amir Pournasserian](https://pournasserian.com/about). I build AI and platform systems for a living, maintain [FluentCMS](https://pournasserian.com/open-source/fluentcms), [YeSvelte](https://pournasserian.com/open-source/yesvelte) and [mcp-my-site](https://pournasserian.com/open-source/mcp-my-site), and write here about what I find along the way.
