Anurag Tiwari

How an MCP client logs in

I’m learning how authorization works in the Model Context Protocol (MCP). The fastest way I found was to run the Rust SDK’s auth examples by hand with curl, and read the spec next to them. This post is what I wrote down.

It follows the MCP authorization spec, version 2026-07-28. Check the spec before you rely on any detail here. The examples I ran are complex_auth_streamhttp.rs and cimd_auth_streamhttp.rs, in the examples/servers/src folder of the Rust SDK. The code there changes, so what you see may differ from what I ran.

Who is who

There are four parties.

  • The client, such as Claude or the MCP Inspector.
  • The resource server, which is the MCP server. It only checks tokens.
  • The authorization server, which logs the user in and issues tokens. It can run in the same process as the resource server, or be a separate service.
  • The browser, where the user logs in and approves.

The whole flow

An MCP client logging in, as a sequence of eleven steps Between the client, the browser, the MCP server and the authorization server. 1 The client sends a request with no token. 2 The MCP server answers 401 with a metadata link. 3 The client fetches the resource metadata. 4 It fetches the authorization server metadata. 5 It gets a client ID. 6 It opens the login page in the browser. 7 The user logs in and approves at the authorization server. 8 The authorization server redirects back to the client with a code, state and iss. 9 The client sends the code, the PKCE verifier and the resource to the authorization server. 10 The authorization server returns an access token. 11 The client calls the MCP server with the token on every request. ClientBrowserMCP serverAuth server 1. request, no token2. 401, metadata link3. get resource metadata4. get auth server metadata6. open login7. user logs in, approves8. redirect: code, state, iss9. code + verifier + resource10. access token11. request with the token 5. get client ID

Steps 1 to 4 are discovery. Steps 5 to 10 are the login. Step 11 is what the resource server sees from then on.

In step 6 the client records the issuer it expects. In step 8 it compares that with the iss it gets back.

Discovery

The client starts knowing one thing: the MCP server’s URL. It sends a request with no token. The server answers 401, and the WWW-Authenticate header carries a resource_metadata URL. The client fetches it. This is the protected resource metadata from RFC 9728, and it names the authorization server. If the header is missing, the client tries the well-known paths instead.

Then the client fetches the authorization server’s own metadata (RFC 8414, or OpenID Connect discovery). That tells it the login, token and registration endpoints. This is a simplification. When the issuer URL has a path, the spec lists three URLs to try in a fixed order, and the client has to try them. The discovery section of the spec has the exact rules.

Two checks happen here, and both matter. The resource in the first document must equal the MCP URL the client called. The issuer in the second must equal the URL the client used to fetch it. The client must also refuse an authorization server that does not advertise PKCE with S256.

Login

The client needs a client ID first. The spec says clients should prefer them in this order.

  1. Pre-registered client information, if the client already has it.
  2. A client ID metadata document, if the authorization server supports it. The client ID is an https URL to a JSON file the client hosts, and the authorization server fetches it during the login. Identity comes from a domain the client controls, and there is nothing to store.
  3. Dynamic client registration (RFC 7591), where the client posts to a /register endpoint. The 2026-07-28 spec deprecates it and keeps it for servers that do not support the metadata document.

If none of these is available, the client asks the user.

Then the client records the issuer it expects, a PKCE verifier and a random state, and opens the browser at the authorization endpoint. The request carries the client ID, the redirect URI, state, the PKCE challenge, the resource and the scope. The user logs in and approves.

The authorization server redirects back to the client’s local callback with a code, the same state and an iss parameter (RFC 9207). The client checks state, and checks iss against the issuer it recorded, before it touches the code. Then it posts the code, the PKCE verifier and the resource to the token endpoint and gets an access token back. The resource goes in both requests, so the token is tied to this MCP server.

After the login

From now on, every request carries Authorization: Bearer <token>. The server checks the signature (or asks the authorization server), the issuer, that the token was meant for this server, that it has not expired, and that its scope is enough. The answer is 200, or 401 for a bad or expired token, or 403 with insufficient_scope when the token is fine but too narrow.

What each piece is for

Piece What it stops
state Someone forging a request to the client’s callback (CSRF)
PKCE A stolen code being used by someone who did not start the login
Exact redirect URI match Codes being sent to an attacker’s URL
iss check A mix-up, where an attacker’s authorization server receives a code meant for an honest one
resource, then the audience check A token for server A being replayed at server B
Token in the header only Tokens ending up in URLs, and then in logs and browser history

Here is the mix-up from that table in a concrete case. Say a client works with several authorization servers, and one of them is run by an attacker. In a mix-up attack, the attacker steers the client so that a code issued by an honest server ends up at the attacker’s token endpoint. The client recorded which issuer it expected before the login, and it compares that with iss when the response comes back. So it sees that the response came from a different server, and it stops.

A detail I liked

A request with no token and a request with a bad token get different answers. With no token, the 401 carries no error. It is only a pointer to where to log in. With a bad or expired token, the 401 carries error="invalid_token". That small difference lets a client tell “you have not logged in” from “log in again” (RFC 6750).

Where to read more

Where I am

I can follow all of this now. The test I set myself is to draw the eleven steps without looking, and say what breaks if each one is skipped. I’m not there yet for every step.