Repository navigation
MCP OAuth2: Unconditional client_id in token request body breaks client_secret_basic with Slack and similar providers #4782
Description
🔴 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:
- 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). - Configure an MCP toolset with OAuth2 using Slack as the provider (or any provider that treats presence of
client_idin body as "authenticate via body" and ignores Basic auth). - Use
OAuth2Authwithclient_idandclient_secretand do not settoken_endpoint_auth_method(so ADK uses defaultclient_secret_basic). - 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:
- ADK Library Version (pip show google-adk): 1.26.0 or later (any version that includes the change from PR fix(oauth): add client id to token exchange #2805)
- Desktop OS: N/A (reproducible on any OS)
- Python Version (python -V): N/A
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_methodisclient_secret_post.
Whentoken_endpoint_auth_methodisclient_secret_basic(or equivalent), do not addclient_idto 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_basicwith Slack (and likely other providers that infer auth from body params).
Activity
weiguangli-io commentedon Mar 11, 2026
Root Cause Analysis
The bug is in
src/google/adk/auth/exchanger/oauth2_credential_exchanger.py, specifically in the_exchange_authorization_codemethod at line 203:client_idis passed as a keyword argument to authlib'sfetch_token(), which adds it to the POST body unconditionally. Meanwhile, theOAuth2Sessionis already constructed (inoauth2_credential_util.py:88-95) withtoken_endpoint_auth_method=auth_credential.oauth2.token_endpoint_auth_method, which defaults to"client_secret_basic". So authlib sends both theAuthorization: Basic ...header andclient_idin the body.RFC 6749 §4.1.3 Violation
Per RFC 6749 §4.1.3:
When
client_secret_basicis used, the client is authenticating per §3.2.1 (HTTP Basic), soclient_idmust not be in the body. Providers like Slack that infer auth method from the presence of body parameters will seeclient_id, treat the request as body-based auth, ignore the Basic header, and reject with "bad client auth."Proposed Fix
Conditionally include
client_idin thefetch_token()call based ontoken_endpoint_auth_method:This keeps the fix from PR #2805 working for
client_secret_postand public clients while avoiding the regression forclient_secret_basic.I'd be happy to submit a PR with this fix if that would be helpful.
joar commentedon Mar 11, 2026
@weiguangli-io
authlib already puts
client_idin the request body when the auth method is either "none" or "client_secret_post" and I think that if there's any doubt whether authlib follows RFC 6749, then the fix should be submitted to authlib. See my comment with the original issue about supposedly missingclient_id: #2806 (comment), which challenges the interpretation of the RFC in that ticket, and also argues that any fixing of RFC-noncompliance would belong in authlib, as authlib explicitly claims to be spec-compliant with RFC 6749.I think the burden of proof should be quite high when the claim is that authlib (which claims to be spec-compliant) isn't spec-compliant.
Reading the description in issue #2806, it only links to one section of the RFC that in turn contains one sentence about
client_id, and that sentence does not support the claim in the issue. The description does not mention a specific OAuth server that the author is trying and failing to authenticate with, so it's hard to know if the root cause was a non-compliant server, or if the author simply enjoys reading specifications, and made a misinterpretation of the spec.joar commentedon Mar 11, 2026
@weiguangli-io additionally, passing
client_idtoclient.fetch_token()would cause the field to be included twice in the request body when the method is "client_secret_post", i.e.,joar commentedon Mar 11, 2026
I have a PR that trusts authlib to decide when and where
client_idis included.saschabuehrle commentedon Mar 11, 2026
Thanks for the clear repro. This is a classic OAuth interoperability edge case.
For client_secret_basic, many providers expect client id and client secret only in Authorization header and reject duplicated client id in body.
A compatibility strategy that usually works:
Respect token_endpoint_auth_method when provider metadata is available
If method is client_secret_basic, omit client id from body
If method is client_secret_post, include both in body
If metadata is missing, try configured method first and avoid automatic fallback unless user enables it
That last part prevents accidental auth method probing across providers. A small matrix test with Slack, Okta, and Auth0 token endpoints can catch regressions early.
joar commentedon Mar 11, 2026
@xuanyang15 (cc because you were involved in merging #2806)
The reasoning in
client_idin request body, andseems to be that
client_idis REQUIRED in request body in order to comply with the spec.The spec says this:
and to an impaired, stressed or unlucky reader, it might look as if
client_idis required always, but the spec doesn't say that. The spec says:this means that, even though I have no doubt the intentions were good, ADK is now out-of-spec, because we pass
client_idin request body all the time, sometimes twice, even though it's not required.if we count
client_idin request body as an authentication method (it's 50% of theclient_secret_postauthentication method), the spec says this:authlib already implements the spec, and passes
client_idin request body when no other token endpoint authentication method is provided.In summary:
client_idall the time.Passing
client_idagainst authlib's advice would be an out-of-spec workaround for out-of-spec providers, and it can help, however, there's no claim that such a provider exists, as it wasn't mentioned in the original issue #2806 or PR #2805, so I will not bother with adding the workaround until we know it's useful - If you know of such a provider, feel free to add the appropriate workaround, just make sure it doesn't affect authlib's decision-making when the workaround isn't active.joar commentedon Mar 11, 2026
@saschabuehrle What you're saying comes off as very general, high-level and unspecific to me. If you have any light to shed on what you're trying to say, please shed that light.
Meanwhile, I'll attempt to figure out what you mean.
With regards to
by "automatic fallback" and "auth method probing", are you
client_secret_basicfortoken_endpoint_auth_method?adk-python/src/google/adk/auth/auth_credential.py
Lines 87 to 94 in ab4b736
token_endpoint_auth_methodtoclient_secret_postwhen your provider requiresclient_secret_post, counts as accidental misconfiguration rather than probing; orilyesJammouci commentedon Mar 12, 2026
Hi maintainers
I'm interested in contributing to this issue
Before getting started, I wanted to confirm whether someone is already working on it. If not, I’d be happy to take it and open a PR
Please let me know if this issue is still available. Thanks!
xuanyang15 commentedon Mar 12, 2026
@ilyesJammouci Thank you! @GWeale Are you working on this?
joar commentedon Mar 12, 2026
In case it's been missed, there is a PR that fixes this issue at #4783
fix: Remove redundant client_id from fetch_token call
davxiao commentedon Mar 16, 2026
I believe this fix maybe the root cause for #4850 as well - it was Okta Oauth2
fix: Remove redundant client_id from fetch_token call
fix: Remove redundant client_id from fetch_token call