Protocols · October 23, 2026 · 11 min read

OAuth for remote MCP servers: the 2026-07-28 auth flow

How a remote MCP server advertises its authorization server, how clients register and get audience-bound tokens, and the mistakes the spec forbids.

A local MCP server is easy to secure because there is barely anything to secure. It runs as a child process on your machine, and it reads an API key from an environment variable. The moment you move that server onto the internet so that other people's agents can reach it, you have a real problem: some unknown client, running on someone else's laptop, needs to prove that a specific person said it could read that person's files. The Model Context Protocol answers that question with OAuth, and the 2026-07-28 authorization specification is the most complete version of that answer so far. This post walks through the flow from the first rejected request to the first successful tool call, explains the two changes in this revision that matter most to builders, and covers the mistakes the spec goes out of its way to forbid.

If you are new to MCP itself, start with our plain-English guide to what an MCP server is, and if you are still digesting the larger transport changes in this revision, our post on the stateless 2026-07-28 revision covers those. Everything below is about who gets in, not how messages travel.

Who needs this, and who does not

Authorization is optional in MCP, and the spec is specific about where it applies. Implementations that use an HTTP-based transport should follow the authorization spec, while implementations using stdio "SHOULD NOT follow this specification, and instead retrieve credentials from the environment." The official authorization tutorial explains the reasoning: a stdio server runs locally and has plenty of other ways to get credentials, whereas OAuth flows are designed for remotely hosted servers where the client has to establish that a user is allowed in. The same tutorial recommends adding authorization when your server touches user-specific data such as email or documents, when you need an audit trail of who did what, when an API needs user consent, or when you want per-user rate limits.

A useful way to think about the roles: your MCP server is an OAuth resource server, the thing holding the data. The MCP client, such as an IDE or a chat app, is an OAuth client acting for a person. A separate authorization server, which might be Keycloak, Auth0, Entra ID, or something you run alongside your server, handles the login screen and issues tokens. The spec deliberately leaves the authorization server's internals out of scope, so your MCP server's job is mostly to point at it and to check the tokens it issues.

Step one: the 401 that tells the client where to go

Everything starts with a failed request. The client sends an ordinary MCP request with no token, and the server answers with HTTP 401 and a WWW-Authenticate header. The spec's example looks like this:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

The resource_metadata value points to a JSON document defined by RFC 9728, OAuth 2.0 Protected Resource Metadata, published in April 2025. MCP servers must implement that RFC, and the document they serve must include an authorization_servers field naming at least one authorization server. RFC 9728 also defines optional fields such as scopes_supported and bearer_methods_supported, so a minimal document for a server at https://mcp.example.com/mcp might contain the resource identifier, one authorization server URL, and the scopes a basic client needs.

The authorization server discovery page lets servers skip the header if they prefer, as long as they serve the metadata at a well-known location instead. For an endpoint at https://example.com/public/mcp, that is either https://example.com/.well-known/oauth-protected-resource/public/mcp or the root https://example.com/.well-known/oauth-protected-resource. Clients must support both: use the header when it is present, and otherwise probe the path-specific URL first and the root second. In practice, sending the header costs you one line and saves every client two guesses, so send it.

The scope parameter in that challenge is a quiet but important detail. The spec tells clients to treat the challenged scopes as authoritative for the current request and to use them first when choosing what to ask for. Only if the challenge carries no scope should a client fall back to everything listed in scopes_supported. The spec also says scopes_supported is meant to represent the minimal set for basic functionality, with more powerful scopes requested later, which is the least-privilege design you want for agents.

Step two: finding the authorization server's endpoints

With an authorization server URL in hand, the client still needs its authorize and token endpoints. MCP leans on two existing discovery formats, RFC 8414 authorization server metadata and OpenID Connect Discovery, and requires clients to try both. For an issuer with a path, such as https://auth.example.com/tenant1, the client tries the OAuth path-insertion URL https://auth.example.com/.well-known/oauth-authorization-server/tenant1 first, then the OpenID path-insertion form, then the OpenID path-appending form. For an issuer without a path, it tries the OAuth URL and then the OpenID one.

Two checks happen at this stage that client authors sometimes skip. First, the issuer inside the fetched document must be identical to the issuer the client used to build the URL, or the client must not use it; the spec's example is a document from attacker.example claiming to be honest.example, which must be rejected. Second, the security considerations page requires clients to confirm PKCE support from that metadata. If code_challenge_methods_supported is absent, the client must refuse to proceed, and it must use the S256 method when it can. A client that cheerfully continues against a server that does not advertise PKCE is out of spec.

Step three: getting a client ID without a signup form

This is where the 2026-07-28 revision changes the most for anyone building a client. In OAuth, a client needs a client ID before it can ask for anything, and MCP's typical case is a client and server that have never heard of each other. The client registration page offers three ways to get one and gives a priority order: use pre-registered credentials if you have them, then Client ID Metadata Documents if the authorization server advertises support, then Dynamic Client Registration as a fallback, and finally ask the user to type client details in.

Client ID Metadata Documents, defined in an IETF draft, flip the registration model around. Instead of the client posting itself to each authorization server, the client hosts a JSON file at an HTTPS URL and uses that URL as its client ID. When the authorization server sees a URL-shaped client ID, it fetches the file, checks that the client_id inside matches the URL exactly, and checks the requested redirect URI against the file's redirect_uris. The spec's example document lists a client_name, a logo, two localhost callback URLs, the authorization_code grant, and token_endpoint_auth_method of none. Authorization servers advertise support with client_id_metadata_document_supported: true in their metadata.

The 2026-07-28 changelog makes the direction explicit: it deprecates Dynamic Client Registration, defined in RFC 7591, in favor of metadata documents, keeping it only for backward compatibility with servers that have not caught up. If you still rely on dynamic registration, the same changelog adds a requirement that clients send an appropriate application_type, because OpenID Connect defaults a missing value to "web", which can clash with the localhost redirect URIs that desktop and CLI clients use. Desktop apps, mobile apps, CLI tools, and locally hosted web apps should send "native".

There is a practical reason to prefer metadata documents beyond following the spec. The registration page notes that credentials obtained by pre-registration or dynamic registration are bound to the authorization server that issued them. Clients must key them by the server's issuer, must not reuse them with a different authorization server, and must re-register when the protected resource metadata points somewhere new. A metadata-document client ID, by contrast, is a URL you own, so it stays portable across authorization servers with no re-registration at all.

Step four: the authorization request and the token

Now the familiar part of OAuth runs: the client generates PKCE values, opens the browser to the authorization URL, the user logs in and approves, and the browser comes back to the client's callback with a code. Two MCP-specific requirements wrap this exchange.

The first is the resource parameter from RFC 8707, Resource Indicators for OAuth 2.0. Clients must include it in both the authorization request and the token request, it must be the canonical URI of the MCP server, and clients must send it even when the authorization server does not support it. Valid examples in the spec include https://mcp.example.com/mcp and https://mcp.example.com:8443; a bare mcp.example.com or a URI with a fragment is invalid. The spec also asks implementations to drop the trailing slash consistently unless it means something. The point of all this is that the resulting token names exactly one audience, your server, which is what makes the server-side check in the next section possible.

The second is new in this revision: issuer validation on the way back, from RFC 9207. Before sending the user to the browser, the client records the issuer from the validated metadata alongside its PKCE verifier. When the code comes back, an iss parameter in the response must match that recorded value by simple string comparison, with no case folding or trailing-slash cleanup, before the code goes to any token endpoint. If the authorization server advertises authorization_response_iss_parameter_supported as true and the response has no iss, the client rejects it. This defends against mix-up attacks, where a malicious authorization server tricks a client into handing over a code issued by an honest one. The spec says authorization servers only should include iss for now, and that a future revision is expected to make it a must, so emitting and checking it today is cheap insurance.

Step five: every request carries the token, and the server checks it

Once the client has an access token, it sends it as Authorization: Bearer <token> on every HTTP request, never in a query string. On the server side, the rules are short and strict. The server must validate the token, must confirm it was issued specifically for this server as the audience, and must answer invalid or expired tokens with a 401. It "MUST NOT accept or transit any other tokens."

That last phrase rules out a pattern that is tempting when you wrap an existing API. Suppose your MCP server fronts a project-management API. The lazy design takes whatever bearer token arrives and forwards it upstream. The MCP security best practices call this token passthrough and explicitly forbid it. The risks they list include clients bypassing the rate limits and validation your server would apply, upstream logs that show the wrong identity, and a stolen token turning your server into a proxy for data exfiltration. The correct design is two separate tokens: the client's token, audience-bound to your MCP server, and a second token your server obtains as an OAuth client of the upstream API. If your server is a proxy that uses one static client ID with a third-party authorization server, the spec adds that you must get user consent for each dynamically registered client before forwarding, to avoid the confused deputy problem.

When the agent needs more permission mid-task

Agents tend to discover what they need as they go. A coding agent that started by reading files may decide halfway through that it needs to write one. MCP handles this with a scope challenge: the server answers with HTTP 403 and a WWW-Authenticate header carrying error="insufficient_scope", the scopes the operation needs, and the metadata URL again. The spec's example asks for files:write with the description "File write permission required for this operation."

The client should then run a step-up authorization. The important rule is in step two of that flow: the new scope request is the union of what the client already asked for and what the challenge demands, so the user does not lose read access while gaining write access. Servers are told to put every scope an operation needs into one challenge rather than revealing them one at a time, since each round trip means another consent screen. Clients should cap retries and track upgrade attempts so a misconfigured server cannot trap an agent in a loop of browser pop-ups. For agent builders, this is also a natural place to put a human checkpoint, the same idea we covered in approval gates for agent tool calls: a write scope arriving mid-run is exactly the moment a person should see.

The pitfalls worth pinning above your desk

The tutorial's closing section reads like a checklist from people who have reviewed a lot of broken servers. Do not write your own token validation; use a well-tested library. Use short-lived access tokens, and on the authorization server side, the security considerations require refresh token rotation for public clients. Never log Authorization headers, tokens, or codes. Do not accept generic audiences such as api. Pin to a single issuer or tenant unless you are deliberately multi-tenant, and reject tokens from other realms even if the same server signed them. Enforce HTTPS everywhere except localhost during development, and remember that all redirect URIs must be localhost or HTTPS. If you leave dynamic registration on, know that unauthenticated registration means anyone can register any client.

For refresh tokens specifically, the main spec draws a line that is easy to get wrong. Clients may request offline_access when the authorization server lists it, but servers should not put offline_access in their challenge or in scopes_supported, because refresh tokens are not something the resource needs. Clients also must not assume a refresh token will be issued at all, so your agent should handle a plain re-login gracefully when an access token expires.

Where to start

If you run a remote MCP server today, the shortest path to a compliant setup is to pick an authorization server you trust, serve a protected resource metadata document, send the resource_metadata and scope hints on your 401 responses, and validate audience on every request with a maintained library. If you build a client, implement both discovery formats, verify PKCE support and issuer identity, send the resource parameter every time, and prefer Client ID Metadata Documents over dynamic registration. None of it is exotic OAuth; the spec mostly picks a careful subset of existing standards and insists that both sides actually check what they receive. That insistence is what lets a stranger's agent touch your users' data safely.

Sources: MCP Authorization (2026-07-28), Authorization Server Discovery, Client Registration, Authorization Security Considerations, MCP Security Best Practices, MCP 2026-07-28 changelog, Understanding Authorization in MCP tutorial, RFC 9728, RFC 8707, RFC 8414, RFC 9207, RFC 7591, OAuth Client ID Metadata Document draft, and OpenID Connect Discovery 1.0.

Newsletter

The daily agent toolkit.

New tools, MCP servers and frameworks, in one short email every morning. Free, no spam. Sign up and get the free AI agent cost cheat sheet (PDF).