AIARTICLE

MCP formalizes OAuth 2.1 as the authorization protocol for servers exposed via HTTP

The Model Context Protocol specification has detailed, since June 2025, an OAuth 2.1-based authorization flow for MCP servers over HTTP transport. Anyone already exposing tools via agents now has a clear roadmap for fixing unchecked access.

What the specification treats as mandatory

The Authorization section of the Model Context Protocol specification, in the version dated June 18, 2025, details an authorization flow for MCP servers running over HTTP transport. Authorization itself remains OPTIONAL: an MCP server can choose not to require any access control. But when it does exist, the spec stops treating the matter as an implementation choice and starts requiring conformance with a specific set of RFCs.

This changes the scenario for anyone who has already put an MCP server into production with custom authentication, like a custom header or a fixed key. The spec cites four standards as a base: OAuth 2.1 (IETF draft), OAuth 2.0 Authorization Server Metadata (RFC 8414), OAuth 2.0 Dynamic Client Registration (RFC 7591), and OAuth 2.0 Protected Resource Metadata (RFC 9728). An MCP server that uses HTTP MUST implement RFC 9728; an MCP client MUST use that metadata to discover the authorization server.

It's worth noting the split by transport: servers running on STDIO (the typical case of a local tool called by an agent on the user's own desktop) SHOULD NOT follow this flow, and should pull credentials directly from the environment. The problem the spec solves is specifically that of the remote server, accessible via HTTP, that any client can attempt to call.

The discovery flow in practice

The first contact between client and server follows a fixed script. When the client hits a protected route without a token, the server responds with 401 and a WWW-Authenticate header pointing to the resource server's metadata URL, per RFC 9728 Section 5.1. The client MUST know how to parse this header.

From there, the client fetches the Protected Resource Metadata document, which contains the authorization_servers field with at least one authorization server entry. Next, it queries that server's OAuth 2.0 Authorization Server Metadata (RFC 8414) to discover the authorization and token endpoints. If the authorization server supports Dynamic Client Registration (RFC 7591), the client can register itself, without requiring the user to manually create a client ID before using the tool.

A minimal example of an Express server responding to the authorization challenge:

javascript
app.use('/mcp', (req, res, next) => {
 const auth = req.headers['authorization'];
 if (!auth?.startsWith('Bearer ')) {
 res.set(
 'WWW-Authenticate',
 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"'
 );
 return res.status(401).json({ error: 'missing_token' });
 }
 next();
});

app.get('/.well-known/oauth-protected-resource', (req, res) => {
 res.json({
 resource: 'https://mcp.example.com/mcp',
 authorization_servers: ['https://auth.example.com']
 });
});

This pair of routes already covers the discovery part. The heavy lifting is in token validation, described next.

Validating the token: where most people get it wrong

The spec is direct about what a server MUST do when receiving a token: validate that it was issued specifically for that server, by checking the audience (the aud claim), per RFC 8707 Section 2. It's not enough for the token to be valid somewhere; it needs to have been issued FOR that specific MCP server.

The mechanism that guarantees this is the resource parameter, defined by Resource Indicators for OAuth 2.0 (RFC 8707). The client MUST include this parameter in both the authorization request and the token request, identifying the canonical URI of the target MCP server:

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

On the server side, this validation becomes a simple claim check before processing any call:

javascript
function validateToken(decoded) {
 const expectedAudience = 'https://mcp.example.com/mcp';
 if (!decoded.aud?.includes(expectedAudience)) {
 throw new Error('token not issued for this resource');
 }
 return decoded;
}

Ignoring this check is the most dangerous mistake the spec tries to prevent. A server that accepts any valid token, without checking the audience, becomes a target for what the document calls the confused deputy problem: an attacker reuses a legitimate token, issued for another service, and the MCP server processes the call as if it were authentic.

Token passthrough: the practice the spec explicitly forbids

There's a common pattern in MCP server prototypes that the specification treats as forbidden: passing the token received from the client, unmodified, to an upstream API. The spec is categorical:

MCP servers MUST NOT accept or transit any other tokens.

Model Context Protocol specification, Authorization section

If the MCP server needs to call an upstream API on the user's behalf, it must act as an OAuth client of that API, with its own token issued by the upstream authorization server. This upstream token is a separate credential, generated in an independent exchange, not the same bearer token the MCP client sent.

In practice, this means that an MCP server that today simply forwards the received Authorization header to a third-party API (for example, a thin proxy over the GitHub or Google API) is violating the rule, even if it works. The fix requires implementing a second, complete OAuth flow between the MCP server and the upstream provider.

PKCE and the other security requirements that become a checklist

Beyond audience validation, the spec lists a set of requirements that work as a review checklist for anyone fixing an existing server:

  • PKCE is mandatory on the client for every authorization code exchange, preventing code interception;
  • every authorization server endpoint must be served over HTTPS, and redirect URIs can only be localhost or HTTPS;
  • redirect URIs must be pre-registered, and the authorization server MUST validate exact matches, not prefixes;
  • access tokens NEVER go in the URL query string, only in the Authorization: Bearer header;
  • invalid or expired tokens always return 401; lack of scope or permission returns 403.

None of these points is new to anyone who has already worked with OAuth 2.1 in another context. The difference is that the MCP spec ties them specifically to the case of an AI agent calling remote tools, where the cost of a leaked token is higher: it doesn't open access to a page, it opens access to a tool the agent can invoke autonomously.

When it's not worth implementing the entire flow

An MCP server that only runs on STDIO, consumed locally by a desktop client in the same process or machine as the user, is outside the scope of this flow by the spec's own definition: it should pull credentials from the environment, not set up an authorization server. Implementing Protected Resource Metadata and Dynamic Client Registration in this case is work without a return.

The full flow is also overkill for an internal MCP server, used only by trusted processes within a closed network, with no public exposure. In this scenario, a simpler authentication scheme (mTLS, a service token with short rotation) solves the problem with less attack surface than setting up a complete OAuth 2.1 authorization server just to follow the letter of the specification.

Outside these two cases, any MCP server accessible via HTTP and exposed beyond the local network is a direct candidate for a fix: the official Model Context Protocol specification already describes the end-to-end roadmap, and the RFCs it references (8414, 7591, 9728, 8707) already have mature implementations in OAuth libraries for practically every language used in backend development today.

Translated from the Brazilian Portuguese original · Read the original

View profile →