Skip to content
Model Context Protocol (MCP)MCP auth

Senior · Core path · 8 chapters

MCP auth

OAuth flows, authorization servers, token audience binding, and why a token minted for your server must not be replayable against upstream APIs

MCP integrations usually fail at credential boundaries, and a token-scoping mistake can turn one compromised server into access to every upstream API.

Read these first

What you will know

  • Implement the OAuth flow for a remote server

    Handle authorization, callback validation, token exchange, refresh, and failure paths rather than treating the access token as a static configuration value.

  • Reproduce an audience-binding failure by presenting a server-scoped token to an upstream API

    A correctly signed token can still be unusable when its resource or audience claim does not match the receiving API.

  • Show why a token minted for your server must not be replayable upstream

    The blast radius of a stolen credential is bounded only when each service rejects tokens issued for a different resource.

  • Debug an auth failure from the protocol trace

    Correlate HTTP status, WWW-Authenticate challenges, issuer metadata, claims, and token-exchange steps instead of guessing from a generic MCP error.

  • Explain the whole flow at a whiteboard

    The useful explanation connects client registration, authorization-server decisions, token validation, audience checks, and the downstream call into one trust story.


Chapters

  1. 1

    Discovering the authorization server

    A client meets a remote MCP server it has never seen and holds no credentials for. The 401 it gets back carries WWW-Authenticate, which points at the server's protected resource metadata, which names the authorization server — discovery, not configuration. From there this chapter walks the grant end to end: client registration, the authorization request, callback validation, code exchange, refresh, and the failure paths that treating an access token as static config skips entirely.

  2. 2

    PKCE, state and redirect_uri

    Three controls ride along with every authorization-code flow and none of them substitutes for another. PKCE binds the code to the client that began the flow; `state` binds the callback to the session that initiated it; redirect_uri registration binds where a code may be delivered, and it is registered at the authorization server against a client id rather than at whatever proxy sits in between. All three assume the attacker is intercepting somebody else's flow, and holding that assumption explicitly is what lets you recognise, later in this topic, an attack in which the attacker owns the flow and all three controls are satisfied.

  3. 3

    Token audience binding

    A token can be fresh, signed by the expected issuer, and working against the same vendor's REST API, and still be refused by their MCP server with error=\"invalid_token\" and a description of audience mismatch. The aud claim names a resource, and the repair belongs at issuance — the resource parameter on the authorization request — not at the server doing the checking. This chapter reproduces that failure deliberately and states what the check buys.

  4. 4

    Building an MCP resource server

    Everything so far is the client's half. From inside a remote MCP server the same flow reads differently: you publish the protected resource metadata that names your authorization server, you answer an unauthenticated call with the 401 and WWW-Authenticate that points at it, and you validate every incoming token before any tool runs — signature and issuer, the aud claim against your own resource identifier, expiry, and the scopes the specific tool requires. These are the checks the rest of the topic assumes are in place.

  5. 5

    Token passthrough to upstream services

    Your server receives a bearer token and passes it unchanged to an internal reporting API, which accepts it because both sit behind the same identity provider. That saves a grant and dissolves a boundary: anyone holding a token for your server now holds one for reporting too, and the audience check stops bounding anything. This chapter ranks the passthrough against a hardcoded Stripe key, decides which to fix first, and answers the claim that nothing was escalated.

  6. 6

    Per-user upstream grants

    The server must call Salesforce for a user who has already authenticated to it and whose tools are working. The answer is not to reuse the incoming token: the server holds its own authorization relationship with Salesforce as a client, stores those tokens per user, and keys them to the identity in the validated request. Two separate grants, no credential doing both jobs — and this is the picture you draw when asked to explain the whole trust story.

  7. 7

    The confused deputy problem

    Register one OAuth client per SaaS, share the client id across every user, and proxy the authorization flows — the fastest shape, and an exploitable one. An attacker starts the flow with a redirect_uri they control, the victim's browser already carries consent for that static client id, so the consent screen is skipped and a code lands on the attacker's host. PKCE, state validation and redirect allowlisting at your proxy are each necessary and none of them touches this. The mitigation is consent you own: your proxy asks each user to authorise it rather than inheriting an approval recorded against a shared client id.

  8. 8

    Diagnosing 401 and 403 responses

    initialize succeeds, tools/list returns, every tools/call comes back 401, and the team has already re-run OAuth twice and rotated the client secret. This chapter sets out the order to pull evidence out of the protocol trace and what each item eliminates, including why success on the earlier calls proves nothing when those endpoints are often unauthenticated. A refresh reissues what the grant already covers and never adds a scope, which is why insufficient_scope needs consent rather than a retry.

External resources