All posts
AI Operations

OAuth for a Remote MCP Server: What the Spec Actually Requires

DevLift Engineering6 min read

A local MCP server has no authorization problem. It launches over stdio on your machine, as you, reading credentials from your environment. The spec says so explicitly: stdio implementations should not follow the authorization specification at all.

Make that server remote and shared, and authorization stops being a detail. It becomes the design.

The tempting shortcut is a shared service account: one set of credentials, broad permissions, every engineer's agent acting as the same identity. It works on the first day and permanently destroys your audit trail, because every action from every person arrives looking identical.

The spec's answer is OAuth 2.1, assembled from four existing RFCs. This post walks through what it actually requires, in the order a client encounters it.

The three roles

Worth getting straight before anything else, because two of them are frequently the same server and people conflate them.

  • The MCP client (Claude Code, Claude Desktop, Cursor) is the OAuth client.
  • The MCP server is the OAuth resource server. It holds the tools and validates tokens. It does not issue them.
  • The authorization server interacts with the user and issues tokens.

The last two can live in the same process or be completely separate. The spec deliberately leaves the authorization server's implementation out of scope. What it standardises is how a client finds it.

How a client discovers everything from a 401

This is the part that makes the whole thing usable, and it is worth understanding because it means no manual configuration anywhere.

The client makes an MCP request with no token. The server returns 401 Unauthorized with a WWW-Authenticate header pointing at its metadata document. That header is required, not optional.

The client then reads Protected Resource Metadata (RFC 9728) from the server:

GET /.well-known/oauth-protected-resource

That document must contain an authorization_servers field naming at least one authorization server. It can name several, in which case choosing between them is the client's job.

From there the client fetches Authorization Server Metadata (RFC 8414) to learn the endpoints:

GET /.well-known/oauth-authorization-server

Now it has everything: where to send the user, where to exchange the code, what the server supports.

Both of these are hard requirements. Servers must implement RFC 9728, clients must use it. Authorization servers must provide RFC 8414, clients must use it. There is no fallback path where a user pastes an endpoint into a config file.

Dynamic client registration, and why it matters here

Normally an OAuth client is registered by hand: a developer creates an app, gets a client ID, ships it.

That does not work for MCP. A client cannot know in advance every MCP server a user might connect to, and asking a user to register an OAuth app before they can add a server is friction nobody will tolerate.

So the spec says clients and authorization servers should support Dynamic Client Registration (RFC 7591). The client posts to a registration endpoint and gets a client ID back with no human involved.

Note the word: should, not must. If your authorization server does not support it, clients are left with two unpleasant options, both explicitly acknowledged in the spec: hardcode a client ID for your server specifically, or show the user a form and make them go and register an OAuth client themselves.

If you are building a server you want people to actually connect to, support RFC 7591.

The resource parameter

This is the requirement most likely to be missed, because it does not exist in ordinary OAuth flows people have written before.

Clients must implement Resource Indicators (RFC 8707). The resource parameter has to appear in both the authorization request and the token request, and it must identify the MCP server the token is for:

&resource=https%3A%2F%2Fmcp.example.com

Two details that catch people out.

Clients must send it whether or not the authorization server supports it. No feature detection, no conditional. Always.

The value is a canonical URI, and the rules are specific. https://mcp.example.com/mcp is valid. https://mcp.example.com:8443 is valid. mcp.example.com is not, because it has no scheme. https://mcp.example.com#fragment is not, because fragments are forbidden. Use the most specific URI you can, and prefer the form without a trailing slash.

The point of all this is what comes next.

Audience validation, and the rule most implementations break

A token is not a general permission slip. It is issued for one specific resource server.

An MCP server must validate that a token was issued specifically for it, checking the audience claim. If validation fails, it must return 401. It must not accept a token that names some other service in its audience, no matter how valid that token is.

Then the rule that gets broken most often:

An MCP server must never pass the client's token through to a downstream API.

This feels wasteful the first time you read it. The client gave you a token, the upstream API needs a token, why mint a second one?

Because they are tokens for different things. If your MCP server calls an upstream API, it is acting as an OAuth client to that API, and it needs its own separate token issued by that API's authorization server. Forwarding the client's token means the upstream service receives a credential it will probably trust, on the assumption that somebody validated it. That is the confused deputy problem: your server's trusted position gets borrowed by whoever supplied the token.

The corresponding rule on the client side: clients must not send a token to an MCP server other than one issued by that server's own authorization server.

Three sentences, and between them they close the most common way these systems get compromised.

The rest of the security surface

Shorter, because it is standard OAuth 2.1 practice rather than anything MCP-specific.

PKCE is mandatory. Clients must implement it. It stops an intercepted authorization code from being redeemable by anyone but the original requester.

Every endpoint over HTTPS. Redirect URIs must be HTTPS or localhost, with no exceptions for staging.

Exact redirect URI matching. Registered in advance, validated exactly, no prefix matching. Clients should also use and verify the state parameter and discard anything that comes back with a mismatch.

Short-lived access tokens, with refresh token rotation required for public clients. Tokens leak through logs, caches and screen recordings, and a short lifetime is what limits the damage.

The token goes in the Authorization header, never the query string. And it goes on every single HTTP request, even ones that are part of the same logical session. There is no session cookie to lean on here.

What this buys you

It is worth stating the operational payoff, because the RFC list makes this look like ceremony.

Your existing permissions become the boundary. When the agent acts on a token issued to a specific person, it can do exactly what that person can do. A developer who cannot touch production has an agent that cannot touch production. Not because the system prompt told it not to, but because the token does not permit it. That property survives prompt injection, a confused model, and a bad day. Instructions do not.

Your audit trail stays meaningful. Every action carries a real identity. Six weeks later, when something is wrong, "who asked for this" has an answer.

Revocation works. Someone leaves, you revoke their access once, and their agents stop working everywhere. With a shared service account, you rotate a secret and update every client.

A checklist

If you are building or evaluating a remote MCP server:

  1. Does it return 401 with a WWW-Authenticate header pointing at its resource metadata?
  2. Does it serve Protected Resource Metadata with an authorization_servers field?
  3. Does the authorization server support dynamic client registration?
  4. Does the client send resource in both the authorization and token request?
  5. Does the server reject tokens whose audience is not itself?
  6. When the server calls an upstream API, does it get its own token, or forward the one it was given?

Item six is the one to check first. It is invisible from the outside, it is the easiest to get wrong, and it is the one with real consequences.


DevLift's MCP server implements OAuth 2.1 so an agent provisions as the developer driving it rather than as a shared account, which means your existing permissions are the boundary and your audit trail keeps a real name on every change. Book a walkthrough, or read what an MCP server for AWS actually does and how to provision from Claude Code.

See what this looks like on your own cloud account

DevLift's agents run continuous cost, drift and compliance detection across AWS, Azure and GCP, and propose fixes as reviewable changes, not dashboards. A walkthrough takes 30 minutes.

Schedule a demo

Keep reading