Ctrl K
ComunidadGuías…

test/integration/oauth_client_metadata_documents_test.rb

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
require "test_helper"

class OauthClientMetadataDocumentsTest < ActionDispatch::IntegrationTest
  URL = "https://claude.ai/oauth/mcp-oauth-client-metadata"
  CALLBACK = "https://claude.ai/api/mcp/auth_callback"
  CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

  setup do
    WebMock.disable_net_connect!(allow_localhost: true)
    stub_dns_resolution("160.79.104.10")
    stub_request(:get, URL).to_return \
      status: 200,
      body: { client_id: URL, client_name: "Claude", redirect_uris: [ CALLBACK ], scope: "read write" }.to_json,
      headers: { content_type: "application/json" }
  end

  test "metadata advertises metadata documents only while they're on" do
    untenanted { get "/.well-known/oauth-authorization-server" }
    assert_equal true, response.parsed_body["client_id_metadata_document_supported"]

    with_metadata_documents(false) do
      untenanted { get "/.well-known/oauth-authorization-server" }
      assert_not response.parsed_body.key?("client_id_metadata_document_supported")
    end
  end

  test "the consent screen renders from the document without registering the client" do
    sign_in_as :david

    untenanted { get new_oauth_authorization_path, params: authorization }

    assert_response :success
    assert_select "h1", text: "Connect this app to Fizzy?"
    assert_select ".oauth-client__name bdi", text: "Claude"
    assert_select ".oauth-client__byline", text: "Name set by the app at claude.ai"
    assert_select ".oauth-client__warning", count: 0
    assert_nil Oauth::Client.find_presented(URL)
  end

  test "consenting provisions the client, and its code redeems for tokens" do
    sign_in_as :david

    untenanted { post oauth_authorization_path, params: authorization }

    assert_response :redirect
    assert response.location.start_with?("#{CALLBACK}?")
    code = Rack::Utils.parse_query(URI.parse(response.location).query)["code"]
    assert Oauth::Client.find_presented(URL).metadata_document?

    untenanted do
      post oauth_token_path, params: {
        grant_type: "authorization_code", client_id: URL, code: code, redirect_uri: CALLBACK, code_verifier: CODE_VERIFIER
      }, as: :json
    end

    assert_response :success
    assert_equal "read write", response.parsed_body["scope"]
    assert_predicate response.parsed_body["refresh_token"], :present?
  end

  test "switching who is signed in registers nothing" do
    sign_in_as :david

    untenanted { post oauth_authorization_switch_path, params: authorization }

    assert_response :redirect
    assert_nil Oauth::Client.find_presented(URL)
  end

  test "Connected apps names the host that set a metadata document client's name" do
    sign_in_as :david
    untenanted { post oauth_authorization_path, params: authorization }
    identities(:david).access_tokens.create! oauth_client: Oauth::Client.find_presented(URL), permission: :read

    untenanted { get my_connected_apps_path }

    assert_select ".connected-app", text: /Name set by the app at claude\.ai/
  end

  test "denying registers nothing" do
    sign_in_as :david

    untenanted { post oauth_authorization_path, params: authorization.merge(error: "access_denied") }

    assert_response :redirect
    assert_equal "access_denied", Rack::Utils.parse_query(URI.parse(response.location).query)["error"]
    assert_nil Oauth::Client.find_presented(URL)
  end

  test "denying against a stale row that allows less than the document still answers access_denied" do
    Oauth::Client.create! client_id: URL, name: "Claude", redirect_uris: [ CALLBACK ], scopes: %w[ read ], client_id_metadata_document: true,
      metadata_fetched_at: 2.days.ago, metadata_expires_at: 1.day.ago
    sign_in_as :david

    untenanted { post oauth_authorization_path, params: authorization.merge(scope: "read write", error: "access_denied") }

    assert_response :redirect
    assert_equal "access_denied", Rack::Utils.parse_query(URI.parse(response.location).query)["error"]
  end

  test "a host that isn't allowlisted is an unknown client, and never fetched" do
    sign_in_as :david
    url = "https://tools.example.com/client.json"
    stub = stub_request(:get, url)

    untenanted { get new_oauth_authorization_path, params: authorization.merge(client_id: url, redirect_uri: "https://tools.example.com/cb") }

    assert_response :bad_request
    assert_select "p", text: Oauth::AuthorizationsController::UNKNOWN_CLIENT_MESSAGE
    assert_not_requested stub
  end

  test "a document that can't be fetched refuses the authorization" do
    sign_in_as :david
    stub_request(:get, URL).to_return(status: 404)

    untenanted { get new_oauth_authorization_path, params: authorization }

    assert_response :bad_request
    assert_select "h2", text: "Can't authorize this app"
    assert_select "[data-oauth-error=?]", "invalid_client_metadata"

    untenanted { get new_oauth_authorization_path(format: :json), params: authorization }

    assert_response :bad_request
    assert_equal "invalid_client_metadata", response.parsed_body["error"]
  end

  test "while off, a URL client_id is an unknown client, provisioned or not" do
    sign_in_as :david
    untenanted { post oauth_authorization_path, params: authorization }
    assert Oauth::Client.find_presented(URL)

    with_metadata_documents(false) do
      untenanted { get new_oauth_authorization_path, params: authorization }
    end

    assert_response :bad_request
    assert_select "p", text: Oauth::AuthorizationsController::UNKNOWN_CLIENT_MESSAGE
  end

  test "a client_id array never reaches a metadata document client" do
    sign_in_as :david
    untenanted { post oauth_authorization_path, params: authorization }
    assert Oauth::Client.find_presented(URL)

    # Refused by the single-valued check before any client lookup, so it
    # neither becomes an IN list nor gets past the switch.
    [ false, true ].each do |enabled|
      with_metadata_documents(enabled) do
        assert_no_queries_match(/oauth_clients/) do
          untenanted { post oauth_authorization_path, params: authorization.merge(client_id: [ URL ]) }
        end
      end

      assert_response :bad_request
      assert_select "[data-oauth-error=?]", "invalid_request"
      assert_select "p", text: OauthHelper::OAUTH_ERROR_MESSAGES["invalid_request"]
    end
  end

  test "a metadata document client isn't reachable by a client_id that isn't its URL" do
    sign_in_as :david
    untenanted { post oauth_authorization_path, params: authorization }
    client = Oauth::Client.find_presented(URL)
    client.update_columns(client_id: "not-a-url-#{client.id}")

    untenanted { get new_oauth_authorization_path, params: authorization.merge(client_id: client.client_id) }

    assert_response :bad_request
    assert_select "p", text: Oauth::AuthorizationsController::UNKNOWN_CLIENT_MESSAGE
  end

  test "an unvetted redirect gets no error redirect before consent" do
    sign_in_as :david

    untenanted { get new_oauth_authorization_path, params: authorization.merge(code_challenge_method: "plain") }

    assert_response :bad_request
    assert_select "[data-oauth-error=?]", "invalid_request"
  end

  teardown do
    WebMock.allow_net_connect!
  end

  private
    def authorization
      {
        client_id: URL,
        redirect_uri: CALLBACK,
        response_type: "code",
        code_challenge: Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false),
        code_challenge_method: "S256",
        scope: "read write",
        state: "xyz"
      }
    end

    def with_metadata_documents(enabled)
      config = Rails.application.config.x.oauth
      saved = config.cimd_enabled
      config.cimd_enabled = enabled
      yield
    ensure
      config.cimd_enabled = saved
    end
end