Repository navigation
Accept self-registered redirects only on loopback, cursor:// and claude://, and allowlisted https hosts #3229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
0367b0e
Accept self-registered redirects only on loopback, cursor:// and clau…
jeremy 5363306
Require a host on app-scheme redirects, and http for loopback
jeremy 4530eba
Merge oauth-token-digests: refuse non-scalar OAuth parameters (#3236)
jeremy 5e36b45
Merge oauth-token-digests: main, with the crass 1.0.8 bump
jeremy 0330e09
Merge oauth-token-digests: filter the code parameter by its whole name
jeremy 6b22ef7
Merge oauth-token-digests: redact retired refresh tokens from logged SQL
jeremy d9c1872
Pin that DCR refuses callbacks on hosts 37signals runs
jeremy a581962
Document OAUTH_DCR_ALLOWED_HTTPS_HOSTS for self-hosted installs
jeremy 6066a97
Merge oauth-token-digests: its migrations can be run again after stop…
jeremy a91924a
Merge oauth-token-digests
jeremy f82eba9
Merge oauth-token-digests into oauth-dcr-allowlist
jeremy e4f9098
Merge oauth-cimd into oauth-dcr-allowlist, so they land on main in order
jeremy 3556921
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 5f6d27d
Merge oauth-cimd into oauth-dcr-allowlist
jeremy feb6354
Merge oauth-cimd into oauth-dcr-allowlist
jeremy b55334b
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 2f78a60
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 338ef90
Merge oauth-cimd into oauth-dcr-allowlist
jeremy c94afd5
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 2ef264e
Merge oauth-cimd into oauth-dcr-allowlist
jeremy e9c8514
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 755d7ab
Merge oauth-cimd into oauth-dcr-allowlist
jeremy 6d5ed9a
Merge oauth-cimd into oauth-dcr-allowlist
jeremy d989db0
Merge oauth-cimd: registration actor, exact-id revocation, quoted max…
jeremy f07c950
Merge oauth-cimd: review fixes from below
jeremy 324b5cb
Merge oauth-cimd: review fixes from below
jeremy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| require "test_helper" | ||
|
|
||
| class OauthDcrRedirectAllowlistTest < ActionDispatch::IntegrationTest | ||
| include OauthAvailabilityTestHelper | ||
|
|
||
| test "registration accepts https only on an allowed host" do | ||
| with_dcr_allowed_https_hosts "claude.ai", "chatgpt.com" do | ||
| register "https://claude.ai/api/mcp/auth_callback" | ||
| assert_response :created | ||
|
|
||
| register "https://connector.example.com/callback" | ||
| assert_response :bad_request | ||
| assert_equal "invalid_redirect_uri", response.parsed_body["error"] | ||
| assert_match "https redirects on claude.ai, chatgpt.com", response.parsed_body["error_description"] | ||
| end | ||
| end | ||
|
|
||
| test "one unallowed host refuses the whole registration" do | ||
| with_dcr_allowed_https_hosts "claude.ai" do | ||
| assert_no_difference "Oauth::Client.count" do | ||
| register "http://127.0.0.1:8888/callback", "https://claude.ai/api/mcp/auth_callback", "https://elsewhere.example/callback" | ||
| end | ||
|
|
||
| assert_response :bad_request | ||
| assert_equal "invalid_redirect_uri", response.parsed_body["error"] | ||
| end | ||
| end | ||
|
|
||
| # Consent names a self-registered app by where its access returns, so a | ||
| # callback on a host we run would read as ours. The allowlist names only | ||
| # vendor hosts (production's, from saas/config/deploy.yml), so ours are | ||
| # refused, in any case or subdomain. | ||
| test "registration refuses https callbacks on hosts 37signals runs" do | ||
| with_dcr_allowed_https_hosts "claude.ai", "claude.com", "chatgpt.com", "www.cursor.com", "grok.com" do | ||
| %w[ | ||
| https://app.fizzy.do/callback | ||
| https://FIZZY.do/callback | ||
| https://3.basecamp.com/callback | ||
| https://basecamp.com/callback | ||
| https://launchpad.37signals.com/callback | ||
| https://app.hey.com/callback | ||
| ].each do |uri| | ||
| assert_no_difference "Oauth::Client.count" do | ||
| register uri | ||
| end | ||
|
|
||
| assert_response :bad_request, uri | ||
| assert_equal "invalid_redirect_uri", response.parsed_body["error"], uri | ||
| end | ||
| end | ||
| end | ||
|
|
||
| test "with no allowed hosts, registration takes loopback and app schemes but no https" do | ||
| with_dcr_allowed_https_hosts do | ||
| register "https://claude.ai/api/mcp/auth_callback" | ||
| assert_response :bad_request | ||
| assert_match "no https redirects on this server", response.parsed_body["error_description"] | ||
|
|
||
| register "http://127.0.0.1:8888/callback", "http://[::1]:8888/callback", "http://localhost:8888/callback" | ||
| assert_response :created | ||
| end | ||
| end | ||
|
|
||
| test "registration accepts the cursor and claude app schemes and no others" do | ||
| register "cursor://anysphere.cursor-mcp/oauth/callback", "claude://claude.ai/mcp-auth-callback/sdk" | ||
| assert_response :created | ||
|
|
||
| %w[ com.example.app://callback vscode://callback cursor:callback ].each do |uri| | ||
| register uri | ||
| assert_response :bad_request, uri | ||
| assert_equal "invalid_redirect_uri", response.parsed_body["error"], uri | ||
| end | ||
| end | ||
|
|
||
| test "an app-scheme client gets its code at its app redirect" do | ||
| sign_in_as :david | ||
| register "cursor://anysphere.cursor-mcp/oauth/callback" | ||
| client = Oauth::Client.find_by!(client_id: response.parsed_body["client_id"]) | ||
|
|
||
| untenanted do | ||
| get new_oauth_authorization_path, params: { | ||
| client_id: client.client_id, | ||
| redirect_uri: "cursor://anysphere.cursor-mcp/oauth/callback", | ||
| response_type: "code", | ||
| code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", | ||
| code_challenge_method: "S256", | ||
| scope: "read" | ||
| } | ||
| end | ||
| assert_response :success | ||
| assert_includes response.headers["Content-Security-Policy"], "cursor:" | ||
| assert_select "strong", text: "cursor://anysphere.cursor-mcp" | ||
|
|
||
| untenanted do | ||
| post oauth_authorization_path, params: { | ||
| client_id: client.client_id, | ||
| redirect_uri: "cursor://anysphere.cursor-mcp/oauth/callback", | ||
| response_type: "code", | ||
| code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", | ||
| code_challenge_method: "S256", | ||
| scope: "read", | ||
| state: "xyz123" | ||
| } | ||
| end | ||
|
|
||
| assert_response :redirect | ||
| location = URI.parse(response.location) | ||
| assert_equal "cursor", location.scheme | ||
| assert_equal "anysphere.cursor-mcp", location.host | ||
| assert Rack::Utils.parse_query(location.query)["code"].present? | ||
| end | ||
|
|
||
| test "an app scheme naming a loopback host gets no pre-consent redirect" do | ||
| sign_in_as :david | ||
| register "cursor://localhost/callback" | ||
| assert_response :created | ||
| client = Oauth::Client.find_by!(client_id: response.parsed_body["client_id"]) | ||
|
|
||
| untenanted do | ||
| get new_oauth_authorization_path, params: { | ||
| client_id: client.client_id, | ||
| redirect_uri: "cursor://localhost/callback", | ||
| response_type: "code", | ||
| code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", | ||
| code_challenge_method: "plain", | ||
| scope: "read" | ||
| } | ||
| end | ||
|
|
||
| assert_response :bad_request | ||
| assert_nil response.location | ||
| end | ||
|
|
||
| test "a client registered before its host left the allowlist still authorizes" do | ||
| sign_in_as :david | ||
| client = Oauth::Client.create!(name: "Hosted", redirect_uris: %w[ https://connector.example.com/callback ], dynamically_registered: true) | ||
|
|
||
| with_dcr_allowed_https_hosts do | ||
| untenanted do | ||
| post oauth_authorization_path, params: { | ||
| client_id: client.client_id, | ||
| redirect_uri: "https://connector.example.com/callback", | ||
| response_type: "code", | ||
| code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", | ||
| code_challenge_method: "S256", | ||
| scope: "read", | ||
| state: "xyz123" | ||
| } | ||
| end | ||
| end | ||
|
|
||
| assert_response :redirect | ||
| assert_match %r{\Ahttps://connector\.example\.com/callback\?}, response.location | ||
| assert Rack::Utils.parse_query(URI.parse(response.location).query)["code"].present? | ||
| end | ||
|
|
||
| private | ||
| def register(*redirect_uris) | ||
| untenanted do | ||
| post oauth_clients_path, params: { client_name: "Connector", redirect_uris: redirect_uris }, as: :json | ||
| end | ||
| end | ||
| end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.