MCP spec requires full OAuth 2.1 for remote tool servers
Since June 2025, the official Model Context Protocol specification has detailed an authorization flow based on OAuth 2.1 that teams running MCP servers in production need to review line by line.
What the specification requires, and since when
The Authorization document, published by the Model Context Protocol team in the revision of June 18, 2025 (modelcontextprotocol.io/specification/2025-06-18/basic/authorization), treats authorization as something technically optional in the protocol. But there is one condition that changes everything: if the transport is HTTP, the specification says the implementation SHOULD follow the described flow; if it is STDIO (the case of local MCP servers), it SHOULD NOT follow that flow and should take credentials directly from the environment.
In practice, this separates two worlds. A local MCP server, running on the developer's machine and talking over stdio, needs none of this: it reads an API_KEY from the .env and gets on with life. A remote server, exposed over HTTP to multiple agents and multiple users, is automatically obliged to implement OAuth 2.1 in full, not an improvised bearer token.
In short: whoever exposes tools over HTTP to third-party agents no longer has room for simplified authentication. The spec binds the server to the role of an OAuth 2.1 resource server, with specific requirements for discovery, token validation and protection against improper reuse.
Discovery: the client needs to find your authorization server on its own
Before thinking about login, the remote MCP server needs to publish where the authorization server responsible for issuing tokens for it is located. This is done via Protected Resource Metadata, defined in RFC 9728, which the spec makes mandatory (MUST) for every MCP server.
In practice, this means publishing a document at /.well-known/oauth-protected-resource with an authorization_servers field listing at least one valid token issuer. When a client tries to access the server without a token and receives a 401, the response must carry the WWW-Authenticate header pointing to that metadata URL:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"If the server that is already in production today simply returns a generic 401 without that header, it is already out of compliance with the current reading of the spec. It is the first point to fix in a migration: before any token logic, make sure the client can discover the path on its own.
The resource parameter: binding the token to the right server
An environment with several MCP servers behind the same authorization server creates an obvious risk: a token issued for server A being accepted by server B. The spec closes that gap by requiring (MUST) that the client send the resource parameter, as per RFC 8707, both in the authorization request and in the token exchange.
The value of that parameter is the canonical URI of the MCP server, without a fragment and preferably without a trailing slash:
GET /authorize?response_type=code&client_id=abc123&resource=https%3A%2F%2Fmcp.example.com&...If the server was already in production before that requirement, it is common for the authorization server to simply ignore the resource parameter, issuing generic tokens that work for any resource. That is exactly the behavior the spec wants to eliminate, because a token without a defined audience is a token that can be reused in places where it should not work.
PKCE and redirect URI: no exception for desktop or mobile
The specification requires (MUST) that every MCP client implement PKCE, even in clients that would normally be treated as confidential, such as desktop apps. The rationale comes straight from OAuth 2.1: an intercepted authorization code is useless without the original verifier.
Two details that tend to catch implementations migrated in a hurry:
- Every authorization server endpoint must be on HTTPS, without exception.
- The redirect URI can only be
localhostor HTTPS; any other scheme is rejected by the spec.
Servers coming from a more permissive OAuth 2.0 flow, with redirect URIs over plain HTTP for the test environment, need to change that before going to production under the new reading of the spec.
Validation on the server: the most common mistake is accepting the wrong token
The part that generates the most vulnerability in the migration is not the login flow, it is what the server does after receiving the token. The spec is direct: the MCP server MUST validate that the token was issued specifically for it, checking the audience claim, and MUST NOT pass along a token received from the client.
This second point is the so-called confused deputy problem, described explicitly in the text of the specification: if the MCP server acts as a proxy to an upstream API, it needs to obtain a separate token, issued by the upstream authorization server, and never forward the original token from the MCP client. Skipping that step is the implementation mistake most cited in the security section of the document.
A middleware skeleton for a Node server illustrates the central point, audience validation before processing any tool call:
async function validateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader?.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' });
}
const token = authHeader.slice(7);
const claims = await verifyJwt(token); // validates signature and expiration
if (claims.aud !== 'https://mcp.example.com') {
// the token exists and is valid, but was not issued for THIS server
return res.status(403).json({ error: 'invalid_audience' });
}
req.user = claims.sub;
next();
}Note the distinction between 401 and 403 in that snippet: a missing or invalid token is 401; a valid token, but with the wrong audience or insufficient scope, is 403. The error table in the specification itself reinforces that separation:
| Code | Situation |
|---|---|
| 401 Unauthorized | Missing authorization or invalid/expired token |
| 403 Forbidden | Invalid scope or insufficient permissions |
| 400 Bad Request | Malformed authorization request |
When it is not worth implementing this
An MCP server that only runs locally, talking over stdio to a client on the same machine, gains nothing by implementing this entire flow. The spec itself recommends the opposite (SHOULD NOT) and suggests taking credentials from the environment, which is simpler and does not introduce a dependency on an external authorization server.
It is also worth weighing the case of short-lived internal prototypes, where the cost of maintaining Protected Resource Metadata, Dynamic Client Registration and refresh token rotation outweighs the real risk of exposure. The spec makes it clear that Dynamic Client Registration is SHOULD, not MUST: you can hardcode a fixed client ID while the server does not need to dynamically accept unknown clients.
For those who already have a remote MCP server running with simplified authentication, the path I would follow is: first publish the Protected Resource Metadata and the WWW-Authenticate header, then enforce the resource parameter in the authorization server, and only then review the audience validation in the code that processes each call. Doing it in the reverse order leaves the system accepting generic tokens while the rest of the migration is still under way.
Translated from the Brazilian Portuguese original · Read the original
Asana Cuts Browsing Agent Cost by 76x With GPT-6.1 Sol
An internal study with 144 runs showed the bottleneck wasn't the chosen model, but how the agent managed cache and browsing history. OpenAI published the case as an example of optimization driven by a coding agent.