test/integration/oauth_error_pages_test.rb
Rama main · réplica de lectura del repositorio original.
require "test_helper"
# The browser authorization flow answers a person with a page, in plain words,
# whatever refused the request; an app that asks for JSON, and every endpoint
# an app calls directly, keeps the OAuth answer it always had.
class OauthErrorPagesTest < ActionDispatch::IntegrationTest
include OauthAvailabilityTestHelper
METADATA_DOCUMENT_URL = "https://claude.ai/oauth/missing-client-metadata"
setup do
sign_in_as :david
end
test "an app not registered for the code grant is a page, not JSON" do
get_authorization client_id: Oauth::Client::PERSONAL_ACCESS_CLIENT_ID
assert_error_page "unauthorized_client", OauthHelper::OAUTH_ERROR_MESSAGES["unauthorized_client"]
get_authorization client_id: Oauth::Client::PERSONAL_ACCESS_CLIENT_ID, format: :json
assert_oauth_json "unauthorized_client", :bad_request
end
test "a metadata document that can't be fetched is a page, and JSON when asked for" do
WebMock.disable_net_connect!(allow_localhost: true)
stub_dns_resolution("160.79.104.10")
stub_request(:get, METADATA_DOCUMENT_URL).to_return(status: 404)
get_authorization client_id: METADATA_DOCUMENT_URL, redirect_uri: "https://claude.ai/api/mcp/auth_callback"
assert_error_page "invalid_client_metadata", OauthHelper::OAUTH_ERROR_MESSAGES["invalid_client_metadata"]
get_authorization client_id: METADATA_DOCUMENT_URL, redirect_uri: "https://claude.ai/api/mcp/auth_callback", format: :json
assert_oauth_json "invalid_client_metadata", :bad_request
end
test "a person reads the plain words, never the developer's description" do
get_authorization redirect_uri: "http://evil.example.com/steal"
assert_error_page "invalid_request", "The app asked to send you back to an address it never registered with Fizzy."
assert_no_match "Invalid redirect_uri", response.body
end
test "an error with nothing particular to say reads as its code's sentence" do
client = Oauth::Client.create!(name: "Hosted", redirect_uris: %w[ https://connector.example.com/callback ], dynamically_registered: true)
{
{ code_challenge: nil } => [ "invalid_request", "code_challenge is required" ],
{ response_type: "token" } => [ "unsupported_response_type", "Only 'code' response_type is supported" ],
{ scope: "admin" } => [ "invalid_scope", "Requested scope is not allowed" ],
{ resource: "https://unknown.example.com/mcp" } => [ "invalid_target", "Unknown resource" ]
}.each do |overrides, (error, description)|
get_authorization client_id: client.client_id, redirect_uri: client.redirect_uris.first, **overrides
assert_error_page error, OauthHelper::OAUTH_ERROR_MESSAGES.fetch(error)
assert_no_match description, response.body
end
end
test "a browser and a wildcard Accept get the page; an app asking for JSON or anything else gets JSON" do
get_authorization client_id: "nonexistent", headers: { "Accept" => "text/html,application/xhtml+xml,*/*;q=0.8" }
assert_error_page "invalid_request", Oauth::AuthorizationsController::UNKNOWN_CLIENT_MESSAGE
get_authorization client_id: "nonexistent", headers: { "Accept" => "*/*" }
assert_error_page "invalid_request", Oauth::AuthorizationsController::UNKNOWN_CLIENT_MESSAGE
get_authorization client_id: "nonexistent", headers: { "Accept" => "application/json" }
assert_oauth_json "invalid_request", :bad_request
get_authorization client_id: "nonexistent", headers: { "Accept" => "application/xml" }
assert_oauth_json "invalid_request", :bad_request
end
test "a dark server tells a person it's not available yet, and gives an app a bare 404" do
with_oauth_availability acceptance: false do
get_authorization
assert_response :not_found
assert_equal "text/html", response.media_type
assert_select "h2", text: "Not available yet"
get_authorization format: :json
assert_response :not_found
assert_empty response.body
end
end
test "paused issuance tells a person so, and an app gets a bare 503" do
with_oauth_availability acceptance: true, issuance: false do
get_authorization
assert_response :service_unavailable
assert_select "h2", text: "Connecting apps is paused"
get_authorization format: :json
assert_response :service_unavailable
assert_empty response.body
end
end
test "endpoints apps call keep their bare statuses whatever they accept" do
with_oauth_availability acceptance: true, issuance: false do
untenanted { post oauth_token_path, params: { grant_type: "authorization_code", code: "x" } }
assert_response :service_unavailable
assert_empty response.body
untenanted { post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/callback" ] }, headers: { "Accept" => "text/html" } }
assert_response :service_unavailable
assert_empty response.body
end
with_oauth_availability acceptance: false do
untenanted { post oauth_token_path, params: { grant_type: "authorization_code", code: "x" }, headers: { "Accept" => "text/html" } }
assert_response :not_found
assert_empty response.body
end
end
test "switching without a browser session is refused with an OAuth error body" do
token = identities(:david).access_tokens.create!(permission: :write)
reset!
untenanted do
post oauth_authorization_switch_path, params: authorization_params, as: :json, headers: { "Authorization" => "Bearer #{token.raw_token}" }
end
assert_oauth_json "access_denied", :forbidden
end
private
def get_authorization(format: nil, headers: {}, **overrides)
untenanted { get new_oauth_authorization_path(format: format), params: authorization_params(**overrides).compact, headers: headers }
end
def authorization_params(**overrides)
{
client_id: oauth_clients(:mcp_client).client_id,
redirect_uri: "http://127.0.0.1:8888/callback",
response_type: "code",
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
code_challenge_method: "S256",
scope: "read",
state: "xyz123"
}.merge(overrides)
end
def assert_error_page(error, message)
assert_response :bad_request
assert_equal "text/html", response.media_type
assert_select "h2", text: "Can't authorize this app"
assert_select "p", text: message
assert_select "[data-oauth-error=?]", error
end
def assert_oauth_json(error, status)
assert_response status
assert_equal "application/json", response.media_type
assert_equal error, response.parsed_body["error"]
end
end