Ctrl K
ComunidadGuías…

test/integration/oauth_single_valued_parameters_test.rb

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

Archivo original ↗Preguntar a KiBot
require "test_helper"

# RFC 6749 §3.1: request parameters MUST NOT be included more than once. Rails
# parses a name[]= form field or a JSON array or object into an Array or a
# nested Parameters, so each endpoint refuses that shape before any lookup.
class OauthSingleValuedParametersTest < ActionDispatch::IntegrationTest
  CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

  setup do
    @client = oauth_clients(:mcp_client)
  end

  # Authorization endpoint

  test "an array client_id is refused before it becomes an IN list" do
    sign_in_as :david

    assert_no_queries_match(/oauth_clients/) do
      untenanted { get new_oauth_authorization_path, params: authorize_params(client_id: [ @client.client_id ]) }
    end

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

  test "an array redirect_uri is shown an error, never redirected to" do
    sign_in_as :david

    untenanted { get new_oauth_authorization_path, params: authorize_params(redirect_uri: [ "http://127.0.0.1:8888/callback" ]) }

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

  test "an array or object in any other authorization parameter goes back to a vetted client as invalid_request" do
    sign_in_as :david

    { scope: [ "read" ], response_type: [ "code" ], code_challenge: [ CHALLENGE ], code_challenge_method: [ "S256" ], state: { a: "b" } }.each do |name, value|
      untenanted { get new_oauth_authorization_path, params: authorize_params(name => value) }

      assert_response :redirect, name.to_s
      query = URI.decode_www_form(URI.parse(response.location).query).to_h
      assert_equal "invalid_request", query["error"], name.to_s
      assert_match name.to_s, query["error_description"]
      assert_not query.key?("code")
    end
  end

  test "an array in another authorization parameter from an unvetted client is shown here" do
    sign_in_as :david
    @client = Oauth::Client.create!(name: "Hosted", redirect_uris: %w[ https://connector.example.com/callback ], dynamically_registered: true)

    untenanted { get new_oauth_authorization_path, params: authorize_params(redirect_uri: "https://connector.example.com/callback", scope: [ "read" ]) }

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

  test "the consent POST refuses an array client_id" do
    sign_in_as :david

    assert_no_queries_match(/oauth_clients/) do
      untenanted { post oauth_authorization_path, params: authorize_params(client_id: [ @client.client_id ]) }
    end

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

  test "a dark server still answers 404 to a malformed request" do
    with_oauth_acceptance(false) do
      untenanted { get new_oauth_authorization_path, params: authorize_params(client_id: [ @client.client_id ]) }
    end

    assert_response :not_found
  end

  # Token endpoint

  test "the token endpoint refuses an array client_id before any lookup" do
    assert_no_queries_match(/oauth_clients|identity_access_tokens/) do
      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: "fizzy_rt_x", client_id: [ @client.client_id ] }, as: :json
      end
    end

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

  test "the token endpoint refuses an array or object in any single-valued parameter" do
    [
      { grant_type: [ "refresh_token" ], refresh_token: "fizzy_rt_x", client_id: @client.client_id },
      { grant_type: "refresh_token", refresh_token: [ "fizzy_rt_x" ], client_id: @client.client_id },
      { grant_type: "refresh_token", refresh_token: "fizzy_rt_x", client_id: @client.client_id, scope: [ "read" ] },
      { grant_type: "authorization_code", code: { a: "b" }, code_verifier: "v", redirect_uri: "http://127.0.0.1:8888/callback", client_id: @client.client_id }
    ].each do |body|
      untenanted { post oauth_token_path, params: body, as: :json }

      assert_response :bad_request, body.inspect
      assert_equal "invalid_request", response.parsed_body["error"], body.inspect
    end
  end

  test "a refresh's JSON null scope still means no scope, not a malformed one" do
    untenanted do
      post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: "fizzy_rt_unknown", client_id: @client.client_id, scope: nil }, as: :json
    end

    assert_equal "invalid_grant", response.parsed_body["error"], response.body
  end

  # Revocation endpoint

  test "revocation refuses an array client_id or token_type_hint before any lookup" do
    [ { token: "x", client_id: [ @client.client_id ] }, { token: "x", token_type_hint: [ "access_token" ] } ].each do |body|
      assert_no_queries_match(/oauth_clients|identity_access_tokens/) do
        untenanted { post oauth_revocation_path, params: body, as: :json }
      end

      assert_response :bad_request, body.inspect
      assert_equal "invalid_request", response.parsed_body["error"], body.inspect
    end
  end

  # Dynamic client registration

  test "registration refuses an array in a single-valued parameter, and still takes scope as a list" do
    untenanted do
      post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/cb" ], client_id: [ "chosen" ] }, as: :json
    end

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

    untenanted do
      post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/cb" ], scope: [ "read" ] }, as: :json
    end

    assert_response :created
    assert_equal "read", response.parsed_body["scope"]
  end

  private
    def authorize_params(**overrides)
      {
        client_id: @client.client_id,
        redirect_uri: "http://127.0.0.1:8888/callback",
        response_type: "code",
        code_challenge: CHALLENGE,
        code_challenge_method: "S256",
        scope: "read",
        state: "xyz"
      }.merge(overrides)
    end

    def with_oauth_acceptance(enabled)
      previous = Rails.configuration.x.oauth.acceptance_enabled
      Rails.configuration.x.oauth.acceptance_enabled = enabled
      yield
    ensure
      Rails.configuration.x.oauth.acceptance_enabled = previous
    end
end