Skip to content

Auth server /token and /revoke return the wrong OAuth error codes (RFC 6749 §5.2) #3723

Open

Description

@Siddharth-DWT

Initial Checks

Release line

2.x (current stable). The same code is on v1.x.

Description

The built-in authorization server's error responses don't match RFC 6749 §5.2, which OAuth 2.1 and RFC 7009 (revocation) reuse:

Case Returns RFC 6749 §5.2
/token, grant the server supports but this client isn't registered for unsupported_grant_type unauthorized_client
/token, grant the server doesn't support (client_credentials, password) invalid_request with pydantic's union error text unsupported_grant_type
/revoke, client authentication fails unauthorized_client invalid_client
/token and /revoke, Basic authentication fails 401 without WWW-Authenticate 401 with WWW-Authenticate matching the scheme ("MUST")

The first two are swapped. The server already knows which grants it supports; it advertises grant_types_supported: ["authorization_code", "refresh_token"] in its metadata. The jwt-bearer path in token.py already returns unauthorized_client for the first case, with a comment saying so (around line 257); the client_info.grant_types check around line 135 returns unsupported_grant_type.

For /revoke, #1481 changed /token to invalid_client for failed client authentication but not RevocationHandler, whose error type only allows invalid_request or unauthorized_client. #1481 also left out WWW-Authenticate because only client_secret_post was supported then; client_secret_basic is supported now.

Clients that branch on the error code, for example to re-register only on invalid_client or to stop retrying on unauthorized_client, behave incorrectly against these responses.

Possible fix: return unauthorized_client at the grant_types check; check grant_type against the supported grants before validating the request union and return unsupported_grant_type; return invalid_client from /revoke; add WWW-Authenticate: Basic to the 401 when the request used Authorization: Basic. I'd be glad to send a PR if this is assigned to me.

Example Code

import base64

from pydantic import AnyHttpUrl
from starlette.applications import Starlette
from starlette.testclient import TestClient

from mcp.server.auth.routes import create_auth_routes
from mcp.server.auth.settings import RevocationOptions
from mcp.shared.auth import OAuthClientInformationFull


class Provider:
    clients = {
        "c1": OAuthClientInformationFull(
            client_id="c1", client_secret="right", redirect_uris=["http://localhost/cb"],
            grant_types=["authorization_code"], token_endpoint_auth_method="client_secret_basic",
        )
    }

    async def get_client(self, client_id):
        return self.clients.get(client_id)

    async def load_refresh_token(self, client, token):
        return None


app = Starlette(routes=create_auth_routes(
    Provider(), AnyHttpUrl("http://localhost:9000"), revocation_options=RevocationOptions(enabled=True)
))


def basic(secret):
    return {"authorization": "Basic " + base64.b64encode(f"c1:{secret}".encode()).decode()}


with TestClient(app) as c:
    for grant in ("refresh_token", "client_credentials"):
        r = c.post("/token", headers=basic("right"), data={"grant_type": grant, "refresh_token": "rt", "client_id": "c1"})
        print("/token", grant, r.status_code, r.json()["error"])
    for path in ("/token", "/revoke"):
        r = c.post(path, headers=basic("WRONG"), data={"grant_type": "refresh_token", "refresh_token": "x", "token": "x", "client_id": "c1"})
        print(path, "wrong secret", r.status_code, r.json()["error"], r.headers.get("www-authenticate"))

Output:

/token refresh_token 400 unsupported_grant_type
/token client_credentials 400 invalid_request
/token wrong secret 401 invalid_client None
/revoke wrong secret 401 unauthorized_client None

Expected unauthorized_client, unsupported_grant_type, then invalid_client with a WWW-Authenticate: Basic ... header on both 401s.

Python & MCP Python SDK

Python 3.13
mcp 2.3.0 (handlers/token.py and handlers/revoke.py identical on main)

Found while testing the auth server endpoints with AI assistance; I ran the reproduction and checked the output myself.

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions