Skip to content

Revocation requires client_secret to be present #3508

Description

@jaworpa

Initial Checks

Release line

2.x (current stable)

Description

What happened?

When Claude Code (or any other public client) triggers revocation - revoke handler requires client_secret to be present, as seen here:

client_secret: str | None

Per OAuth 2.0 specifications (RFC 6749 Section 2.3 and RFC 7009 Section 5), public clients do not have a client_secret and identify themselves using only client_id.
This leads to issue when public clients do not sent client secret at all.

In Pydantic v2, defining a field as str | None without a default value still marks the field as required (i.e., nullable value, but the key must exist in the request body).

Steps to reproduce

  1. Authenticate using a public OAuth client (e.g., Claude Code CLI).
  2. Trigger token revocation (e.g., re-authenticating or logging out).
  3. The authorization server returns a 400 Bad Request with:
    {"error":"invalid_request","error_description":"client_secret: Field required"}

Expected behavior

client_secret should be optional so public clients can revoke tokens without sending a client_secret key.

Fix

Adding default value for client_secret:

client_secret: str | None = None

Workaround

As workaround I'm overriding Request type:

from mcp.server.auth.handlers import revoke

class FixedRevocationRequest(revoke.RevocationRequest):
    client_secret: str | None = None

revoke.RevocationRequest = FixedRevocationRequest

Example Code

from mcp.server.auth.handlers.revoke import RevocationRequest
from pydantic import ValidationError

# Simulate a public client request: client_secret parameter omitted entirely
form_no_secret = {"token": "abc", "client_id": "local-scurri-mcp"}

try:
    req = RevocationRequest.model_validate(form_no_secret)
    print("no-secret: OK ->", req)
except ValidationError as e:
    print("no-secret: FAILS ->", e.errors()[0]["msg"])

Python & MCP Python SDK

MCP version:                                                                                                     2.2.0
Python version:                                                                                                 3.13.9

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 15, 2026
  2. rajeevchandra commented on Sep 16, 2026

    @rajeevchandra

    Confirmed the bug on 2.2.0. RevocationRequest.client_secret is defined as str | None with no default, so Pydantic v2 treats it as required — public clients that omit the field entirely get a 400 {"error":"invalid_request","error_description":"client_secret: Field required"}.

    Fix is one line in src/mcp/server/auth/handlers/revoke.py:
    client_secret: str | None = None

    ClientAuthenticator already handles token_endpoint_auth_method == "none" correctly — this is purely a model validation gap. I'd like to fix it if you're open to an outside PR.

  3. HARSHAVARDHAN-RAJU5 commented on Sep 16, 2026

    @HARSHAVARDHAN-RAJU5

    I confirmed this behavior on 2.x. Omitting client_secret from a /revoke request for a client registered with token_endpoint_auth_method: "none" currently results in a 400 response with client_secret: Field required.

    The cause is that RevocationRequest.client_secret is defined as str | None but does not have a default value, making the field required. ClientAuthenticator already handles public clients correctly, so the fix is to define it as client_secret: str | None = None.

    I’ve implemented this fix and added a regression test that fails before the change and passes after it. The changes are included in #3512.

  4. mulatta commented on Sep 30, 2026

    @mulatta

    Same issue here.

    A dynamically registered public client (token_endpoint_auth_method=none) can complete authorization-code + PKCE, obtain and rotate access/refresh tokens, and call the protected MCP resource.

    Calling /revoke without client_secret then returns: 400 invalid_request, client_secret: Field required

  5. Enzo-TesterQA commented on Oct 10, 2026

    @Enzo-TesterQA

    Adding one related detail from running a public-client flow with claude.ai (CIMD) on 2.3.0, in case it helps whoever picks this up.

    Making client_secret optional in RevocationRequest fixes the 400, but the authorization server metadata built by build_metadata still advertises:

    "revocation_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]

    without "none". A public client (Claude registers with token_endpoint_auth_method: "none" through CIMD or DCR) that reads the metadata can conclude it isn't allowed to revoke, so sessions may stay open after disconnecting. It's the same situation #2260 described for token_endpoint_auth_methods_supported.

    So the fix probably needs both:

    1. client_secret: str | None = None in RevocationRequest.
    2. "none" in revocation_endpoint_auth_methods_supported (and in token_endpoint_auth_methods_supported, for public clients).

    Our workaround on 2.3.0, in case it's useful to others: we replace the /revoke route with a small handler that reuses ClientAuthenticator (it already accepts none), and add "none" to both lists with OAuthMetadata.model_copy(update=...) before serving the metadata.

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