Skip to content

MCP OAuth2: Unconditional client_id in token request body breaks client_secret_basic with Slack and similar providers #4782

@joar

Description

@joar

🔴 Required Information

Please ensure all items in this section are completed to allow for efficient triaging. Requests without complete information may be rejected / deprioritized. If an item is not applicable to you - please mark it as N/A

Describe the Bug:
When using OAuth2 with client_secret_basic (HTTP Basic Auth at the token endpoint), ADK unconditionally includes client_id in the token request body. Per RFC 6749 §4.1.3, client_id in the body is only REQUIRED "if the client is not authenticating with the authorization server as described in Section 3.2.1." When the client is authenticating via HTTP Basic (Section 3.2.1), sending client_id in the body is unnecessary and causes interoperability failures with providers (e.g. Slack) that infer auth method from the request: if they see client_id in the body, they treat the request as body-based auth and ignore the HTTP Basic Auth header, leading to "bad client auth."

Steps to Reproduce:

  1. Install google-adk (version that includes the fix from PR fix(oauth): add client id to token exchange #2805, e.g. 1.26.0 or later).
  2. Configure an MCP toolset with OAuth2 using Slack as the provider (or any provider that treats presence of client_id in body as "authenticate via body" and ignores Basic auth).
  3. Use OAuth2Auth with client_id and client_secret and do not set token_endpoint_auth_method (so ADK uses default client_secret_basic).
  4. Complete the authorization code flow; when ADK exchanges the code for a token, the token endpoint returns an error such as "bad client auth" or similar.

Expected Behavior:
When using client_secret_basic, the token request should authenticate only via the Authorization: Basic ... header (per RFC 6749 §3.2.1) and should not include client_id in the request body, so that providers that key off body parameters do not misinterpret the auth method. Token exchange should succeed.

Observed Behavior:
ADK sends both HTTP Basic Auth and client_id in the token request body. The provider (e.g. Slack) sees client_id in the body, assumes body-based client auth, ignores the Basic header, and rejects the request with "bad client auth" (or equivalent).

Environment Details:

Model Information:

  • Are you using LiteLLM: N/A
  • Which model is being used: N/A

🟡 Optional Information

Providing this information greatly speeds up the resolution process.

Regression:
Yes. This regressed after the merge of PR #2805 ("fix(oauth): add client id to token exchange"), which added client_id to the token request body to address issue #2806. That change was based on a misreading of RFC 6749 §4.1.3: the RFC states that client_id is REQUIRED only "if the client is not authenticating with the authorization server as described in Section 3.2.1". When the client is authenticating via Section 3.2.1 (e.g. HTTP Basic), client_id in the body is not required and should be omitted to avoid breaking providers that infer auth method from the presence of body parameters.

Logs:

// Token exchange fails with 400 or similar; provider returns error like "bad client auth"
// or "invalid_client" when client_secret_basic is used and client_id is present in body.

Additional Context:

  • RFC reference: RFC 6749 §4.1.3 (Access Token Request) explicitly says: "client_id - REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1."
  • Suggested fix: Include client_id (and, for post auth, client_secret) in the token request body only when:
    • the client is not authenticating via Section 3.2.1 (e.g. public client), or
    • token_endpoint_auth_method is client_secret_post.
      When token_endpoint_auth_method is client_secret_basic (or equivalent), do not add client_id to the body.

Minimal Reproduction Code:

from google.adk.auth import AuthCredential, AuthCredentialTypes, OAuth2Auth
from google.adk.auth.auth_schemes import OpenIdConnectWithConfig
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams

# Slack (or similar) OAuth2 with default auth = client_secret_basic.
# ADK sends client_id in body + Basic header; Slack ignores Basic, expects body credentials → "bad client auth".
McpToolset(
    connection_params=StreamableHTTPConnectionParams(url="https://mcp.slack.com/mcp"),
    auth_scheme=OpenIdConnectWithConfig(
        authorization_endpoint=settings.SLACK_MCP.OAUTH2.AUTHORIZATION_URL,
        token_endpoint=settings.SLACK_MCP.OAUTH2.TOKEN_URL,
        scopes=settings.SLACK_MCP.OAUTH2.SCOPES,
    ),
    auth_credential=AuthCredential(
        auth_type=AuthCredentialTypes.OAUTH2,
        oauth2=OAuth2Auth(
            client_id=settings.SLACK_MCP.OAUTH2.CLIENT_ID,
            client_secret=settings.SLACK_MCP.OAUTH2.CLIENT_SECRET,
            # Workaround: force body-based auth so server and client agree.
            # token_endpoint_auth_method="client_secret_post",
        ),
    ),
)

Workaround: Set token_endpoint_auth_method="client_secret_post" on OAuth2Auth so credentials are sent in the body; then the provider’s expectation (body-based auth) matches what ADK sends.

How often has this issue occurred?:

  • Always (100%) when using client_secret_basic with Slack (and likely other providers that infer auth from body params).

Metadata

Metadata

Assignees

Labels

auth[Component] This issue is related to authorization

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions