AIARTICLE

MCP Requires OAuth 2.1 for Remote Servers Since June 2025: Here's What Changes in the Code

The June 18, 2025 revision of the Model Context Protocol specification turns every remote HTTP server into an OAuth 2.1 resource server. Anyone still validating a static token in an environment variable has a concrete migration path, as well as cases where this doesn't apply.

What the Specification Requires, and Since When

The Authorization section of the Model Context Protocol specification, in the June 18, 2025 revision, formalizes a role that was previously implicit: every MCP server exposed via HTTP transport that wants to offer authorization now acts as an OAuth 2.1 resource server. The document is clear about scope: authorization remains optional for MCP implementations, but whoever implements it must follow this design, not invent their own scheme.

It's worth separating two worlds. Servers that run over STDIO (the typical case of a local MCP tool, invoked by a child process) should not follow this specification: the spec's own guidance is for them to pull credentials from the environment, as they always have. The impact falls on whoever exposes a remote MCP server, accessible over HTTP, to multiple clients they don't control.

From Static Token to Resource Server

The most common pattern until now, in prototypes and internal products, was a fixed bearer token stored in an environment variable and compared byte by byte. In code, this usually looks like:

js
app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token !== process.env.MCP_STATIC_TOKEN) {
    return res.status(401).send('Unauthorized');
  }
  next();
});

This code becomes insufficient on two central points of the spec: the server doesn't announce where the authorization server is located (a requirement of RFC9728, Protected Resource Metadata) and doesn't validate who that token was issued for (the audience binding requirement of RFC8707). A static token carries no audience to validate, so it simply doesn't exist in this model.

What the Remote Server Must Now Implement

The migration has specific pieces, and the spec marks most of them as mandatory (MUST), not as a recommendation:

  • A Protected Resource Metadata endpoint (RFC9728), which tells the client where the authorization server is.
  • The WWW-Authenticate header in every 401 response, pointing to this metadata endpoint.
  • Validation that the received token has an audience matching the server's own canonical URI (RFC8707).
  • Support, on the authorization server side, for Dynamic Client Registration (RFC7591) and Authorization Server Metadata (RFC8414), so that new clients don't need a hardcoded client ID.
  • On the client side, mandatory use of PKCE in the authorization code flow.

A migration skeleton on the server looks like this:

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

app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    res.set('WWW-Authenticate',
      'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"');
    return res.status(401).send();
  }
  const claims = verifyJwt(token, {
    audience: 'https://mcp.example.com/mcp',
    issuer: 'https://auth.example.com'
  });
  if (!claims) return res.status(401).send();
  req.user = claims;
  next();
});

The point that most often trips up those coming from the old model: validating audience is neither optional nor cosmetic. Without it, a token issued for a completely different service, but by the same authorization server, would be accepted by the MCP server as if it were valid there.

The "Confused Deputy" Problem and Why Forwarding Tokens Is Forbidden

The spec devotes an entire section to the case of an MCP server that acts as a proxy for a third-party API. If this server accepts the client's token and simply forwards it to the upstream API without revalidating it, it becomes a classic confused deputy target: an attacker with a stolen token can get the downstream API to trust it as if it had legitimately come from the MCP server.

MCP servers MUST NOT accept or transit any other tokens.

Model Context Protocol specification, Authorization section (2025-06-18 revision)

In practice, this means that when the MCP server needs to call an upstream API on behalf of the user, it must act as an OAuth client of that API and obtain its own token, separate from the token it received from the MCP client. It's more code and one more trust relationship to manage, not a simple header pass-through.

When This Migration Isn't Worth It

Not every MCP server needs this tomorrow. The spec itself treats authorization as an optional capability, and the recommendation to skip the flow applies fully to STDIO servers, which continue pulling credentials from the local environment. Internal tools, running behind a corporate VPN and called only by a client the team itself controls, also have a reasonable case for postponing this: the security gain of the full OAuth 2.1 flow matters less when a network layer already restricts access.

The scenario where migration stops being optional, in practice, is the remote, multi-tenant server designed to be discovered and used by third-party MCP clients. In this case, a spec-compliant client will expect exactly the described flow: protected resource metadata, authorization server discovery, PKCE, and tokens with the correct audience. Without this, the connection simply doesn't complete the way the protocol intends.

Translated from the Brazilian Portuguese original · Read the original

View profile →
Read also