Repository navigation
fix(mcp): include OAuth state parameter in authorization URLs - #1049
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves an issue preventing MCP OAuth authorization with servers that strictly enforce the inclusion of the "state" parameter in authorization requests. By programmatically generating and embedding a secure, unique "state" value, the system now ensures broader compatibility with diverse OAuth providers without compromising security, as the primary protection against CSRF and authorization code injection is maintained through PKCE. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request correctly addresses an issue where MCP OAuth authorization fails with servers requiring a state parameter by adding a cryptographically random state to the authorization URL. The change is well-reasoned, affects both pre-configured and dynamic client registration paths, and includes a thorough regression test. My review includes one suggestion to improve the robustness of the random number generation by handling potential errors instead of panicking, which is better practice within a function that already returns a Result.
Some MCP servers (e.g. Attio) require the `state` parameter in OAuth
authorization requests and reject requests without it:
{"error":"invalid_request","error_description":"Invalid value provided for: state"}
While OAuth 2.1 makes `state` optional when PKCE is used, the MCP
specification does not forbid servers from requiring it. This caused a
hard failure when authenticating with any MCP server that enforces the
state parameter.
Generate a 128-bit cryptographically random state (via OsRng, base64url
encoded without padding) and inject it into extra_params before building
the authorization URL. This covers both pre-configured OAuth and Dynamic
Client Registration (DCR) code paths.
The callback listener intentionally does not validate the echoed state
because: (1) PKCE already binds the authorization code to the token
exchange, preventing code injection attacks, and (2) not all MCP servers
echo state back — strict validation would break those servers. Other
OAuth flows in the codebase (tool.rs, extensions/manager.rs) that
generate and validate state are unaffected.
3543ded to
6ca2fc7
Compare
zmanian
left a comment
There was a problem hiding this comment.
Reviewed the diff carefully. This is a clean, well-scoped fix. Approving.
Security assessment:
- State generation is correct: 128 bits from
OsRng(CSPRNG), base64url-encoded without padding. Good entropy, URL-safe, no padding characters that could cause issues. - The insertion point (after both pre-configured and DCR paths converge, before
build_authorization_url) is the right place -- covers both flows with a single code path. - The
muton the destructuring binding is the minimal change needed.
On not validating state on callback:
The rationale in the PR description is sound. PKCE (code_verifier/code_challenge) already binds the authorization code to the token exchange, which is the primary defense against authorization code injection -- the same attack state traditionally mitigates. The wait_for_callback infrastructure already supports Some(&state) validation, so if a future MCP server ecosystem standardizes on echoing state, it would be straightforward to enable. For now, sending state without validating it is a defensible pragmatic choice given the diversity of MCP server implementations.
Minor note: If a user had manually configured "state" in oauth.extra_params, this overwrites it -- but that's actually better behavior since a static configured value would be insecure.
Test coverage: The regression test validates presence in URL, encoding safety, entropy length, and uniqueness. Reasonable coverage for this change.
…#1049) Some MCP servers (e.g. Attio) require the `state` parameter in OAuth authorization requests and reject requests without it: {"error":"invalid_request","error_description":"Invalid value provided for: state"} While OAuth 2.1 makes `state` optional when PKCE is used, the MCP specification does not forbid servers from requiring it. This caused a hard failure when authenticating with any MCP server that enforces the state parameter. Generate a 128-bit cryptographically random state (via OsRng, base64url encoded without padding) and inject it into extra_params before building the authorization URL. This covers both pre-configured OAuth and Dynamic Client Registration (DCR) code paths. The callback listener intentionally does not validate the echoed state because: (1) PKCE already binds the authorization code to the token exchange, preventing code injection attacks, and (2) not all MCP servers echo state back — strict validation would break those servers. Other OAuth flows in the codebase (tool.rs, extensions/manager.rs) that generate and validate state are unaffected.
…#1049) Some MCP servers (e.g. Attio) require the `state` parameter in OAuth authorization requests and reject requests without it: {"error":"invalid_request","error_description":"Invalid value provided for: state"} While OAuth 2.1 makes `state` optional when PKCE is used, the MCP specification does not forbid servers from requiring it. This caused a hard failure when authenticating with any MCP server that enforces the state parameter. Generate a 128-bit cryptographically random state (via OsRng, base64url encoded without padding) and inject it into extra_params before building the authorization URL. This covers both pre-configured OAuth and Dynamic Client Registration (DCR) code paths. The callback listener intentionally does not validate the echoed state because: (1) PKCE already binds the authorization code to the token exchange, preventing code injection attacks, and (2) not all MCP servers echo state back — strict validation would break those servers. Other OAuth flows in the codebase (tool.rs, extensions/manager.rs) that generate and validate state are unaffected.
Problem
MCP OAuth authorization fails with servers that require the
stateparameter (e.g. Attio):The
authorize_mcp_server()function insrc/tools/mcp/auth.rsbuilds the authorization URL without astateparameter. While OAuth 2.1 makesstateoptional when PKCE is used, the MCP specification does not forbid servers from requiring it, and some servers (confirmed: Attio atmcp.attio.com) enforce it as mandatory — rejecting any authorization request that omits it.This affects both the pre-configured OAuth path and the Dynamic Client Registration (DCR) path, since both converge before the authorization URL is built and neither injects a
statevalue.Why
extra_paramsdoesn't solve thisThe pre-configured OAuth path passes through
oauth.extra_paramsfrom the MCP server config, so in theory a user could manually add"state": "<value>"to their config. However:HashMap::new()— there's no config to set extra params for dynamically registered clientsFix
Generate a 128-bit cryptographically random
stateparameter (viaOsRng, base64url-encoded without padding) and inject it intoextra_paramsafter both code paths converge but beforebuild_authorization_url()is called. This is a 6-line change plusmuton the binding.Design decisions
Why not validate state on the callback?
The callback listener (
wait_for_authorization_callback) passesNonefor state validation intentionally:PKCE is the security boundary. The code_verifier/code_challenge exchange already binds the authorization code to the token request, preventing authorization code injection — the primary attack that
statemitigates in non-PKCE flows.Not all MCP servers echo state back. Strict validation would break servers that accept
statebut don't include it in the redirect. Since MCP servers are diverse (custom implementations, not just major OAuth providers), this is a real risk.Other callers are unaffected. The shared
wait_for_callback()inoauth_helpers.rssupports optional state validation. Other OAuth flows in the codebase (cli/tool.rs,extensions/manager.rs) already generate and validate state withSome(&state)— this change doesn't touch those paths.State is sent for compatibility, PKCE provides the security guarantee.
Testing
test_authorization_url_includes_state_parameterverifies state appears in the URL, uses base64url-safe encoding (no+,/,=), has sufficient entropy (≥128 bits), and is unique per generation--all --all-featuresTest plan
cargo test— all unit tests passcargo clippy --all --benches --tests --examples --all-features— zero warningsironclaw mcp auth <server>against an MCP server that requires state (e.g. Attio)ironclaw mcp auth <server>against an MCP server that does NOT require state — verify no regression