test/integration/oauth_client_secret_basic_test.rb
Rama main · réplica de lectura del repositorio original.
require "test_helper"
# client_secret_basic beside client_secret_post (RFC 6749 §2.3.1).
class OauthClientSecretBasicTest < ActionDispatch::IntegrationTest
include OauthAvailabilityTestHelper, OauthClientCredentialsTestHelper
CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
setup do
@client = oauth_clients(:confidential_client)
@secret = "confidential_secret_789"
end
# client_secret_basic at the token endpoint
test "a confidential client refreshes with HTTP Basic alone" do
token = grant_for(@client)
refresh token, headers: basic(@client.client_id, @secret)
assert_response :success
assert_not_equal token.refresh_token_digest, token.reload.refresh_token_digest
assert_equal token.refresh_token_digest, TokenDigester.digest(response.parsed_body["refresh_token"])
end
test "a confidential client redeems a code with HTTP Basic alone" do
code = authorization_code_for(@client)
assert_difference "Identity::AccessToken.count", +1 do
untenanted do
post oauth_token_path, params: { grant_type: "authorization_code", code: code,
redirect_uri: "https://connector.example.com/callback", code_verifier: CODE_VERIFIER },
headers: basic(@client.client_id, @secret)
end
end
assert_response :success
end
test "Basic credentials are form-urlencoded before they are encoded, so %3A and + decode after the split" do
client = Oauth::Client.create! name: "Colon", redirect_uris: %w[ https://connector.example.com/callback ],
token_endpoint_auth_method: "client_secret_basic", raw_client_secret: "pa:ss word+"
token = grant_for(client)
refresh token, headers: basic(client.client_id, "pa:ss word+")
assert_response :success
end
test "Basic beside a body client_id naming the same client authenticates" do
token = grant_for(@client)
refresh token, params: { client_id: @client.client_id }, headers: basic(@client.client_id, @secret)
assert_response :success
end
test "Basic beside an empty body client_secret authenticates" do
token = grant_for(@client)
refresh token, params: { client_secret: "" }, headers: basic(@client.client_id, @secret)
assert_response :success
end
test "Basic and a body client_secret together are two methods, refused as invalid_request" do
token = grant_for(@client)
[ @secret, "something-else" ].each do |body_secret|
refresh token, params: { client_id: @client.client_id, client_secret: body_secret }, headers: basic(@client.client_id, @secret)
assert_response :bad_request
assert_equal "invalid_request", response.parsed_body["error"]
end
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "Basic naming one client while the body names another fails as invalid_client" do
token = grant_for(@client)
refresh token, params: { client_id: oauth_clients(:mcp_client).client_id }, headers: basic(@client.client_id, @secret)
assert_client_authentication_failed
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a malformed Basic header fails as invalid_client" do
token = grant_for(@client)
[ "Basic not-base64!", "Basic #{Base64.strict_encode64("no-colon")}", "Basic #{Base64.strict_encode64(":#{@secret}")}",
"Basic #{Base64.strict_encode64("#{@client.client_id}:%ZZ")}", "Basic #{Base64.strict_encode64("#{@client.client_id}:%FF")}" ].each do |header|
refresh token, headers: { "Authorization" => header }
assert_client_authentication_failed header
end
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a bare Basic scheme is a failed attempt, not an absent one" do
client = oauth_clients(:mcp_client)
token = grant_for(client)
refresh_token_digest = token.refresh_token_digest
personal = identities(:david).access_tokens.create!(description: "Personal")
[ "Basic", "Basic ", "basic" ].each do |header|
refresh token, params: { client_id: client.client_id }, headers: { "Authorization" => header }
assert_client_authentication_failed header
untenanted { post oauth_revocation_path, params: { token: personal.raw_token }, headers: { "Authorization" => header } }
assert_client_authentication_failed header
end
assert Identity::AccessToken.exists?(personal.id)
assert_equal refresh_token_digest, token.reload.refresh_token_digest
end
test "Basic naming a public client fails: a public client holds no password" do
client = oauth_clients(:mcp_client)
token = grant_for(client)
[ "", "anything" ].each do |secret|
refresh token, headers: basic(client.client_id, secret)
assert_client_authentication_failed
end
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a wrong Basic secret and an unknown Basic client get the same 401 and Basic challenge" do
token = grant_for(@client)
refresh token, headers: basic(@client.client_id, "wrong")
wrong_secret = [ response.status, response.parsed_body, response.headers["WWW-Authenticate"] ]
refresh token, headers: basic("no-such-client", "wrong")
unknown_client = [ response.status, response.parsed_body, response.headers["WWW-Authenticate"] ]
assert_client_authentication_failed
assert_equal wrong_secret, unknown_client
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a failed client_secret_post authentication is a 400 with no challenge, since the client never tried the header" do
token = grant_for(@client)
[ { client_id: @client.client_id, client_secret: "wrong" }, { client_id: @client.client_id } ].each do |params|
refresh token, params: params
assert_client_authentication_failed params.inspect, basic: false
assert_equal "no-store", response.headers["Cache-Control"]
end
end
test "a failed client_secret_post beside an Authorization header of another scheme is a 400: Basic was never evaluated" do
token = grant_for(@client)
refresh token, params: { client_id: @client.client_id, client_secret: "wrong" }, headers: { "Authorization" => "Bearer #{token.raw_token}" }
assert_client_authentication_failed basic: false
end
test "the pilot gate reads the client a Basic header names" do
token = grant_for(@client)
with_oauth_availability acceptance: false, issuance: false, pilot_client_ids: [ @client.client_id ] do
refresh token, headers: basic(@client.client_id, @secret)
end
assert_response :success
end
test "every advertised secret method authenticates at the token endpoint" do
untenanted { get "/.well-known/oauth-authorization-server" }
methods = response.parsed_body["token_endpoint_auth_methods_supported"]
assert_equal %w[ none client_secret_post client_secret_basic ], methods
{ "client_secret_post" => [ { client_id: @client.client_id, client_secret: @secret }, {} ],
"client_secret_basic" => [ {}, basic(@client.client_id, @secret) ] }.each do |method, (params, headers)|
refresh grant_for(@client), params: params, headers: headers
assert_response :success, method
end
end
test "DCR registers a client_secret_basic client as confidential" do
untenanted do
post oauth_clients_path, params: { client_name: "Basic Connector", redirect_uris: %w[ https://connector.example.com/callback ],
token_endpoint_auth_method: "client_secret_basic" }, as: :json
end
assert_response :created
assert_equal "client_secret_basic", response.parsed_body["token_endpoint_auth_method"]
assert_not_nil response.parsed_body["client_secret"]
assert Oauth::Client.find_by(client_id: response.parsed_body["client_id"]).confidential?
end
private
def grant_for(client)
identities(:david).access_tokens.create!(oauth_client: client, permission: :read)
end
def refresh(token, params: nil, headers: {})
params ||= headers.key?("Authorization") ? {} : { client_id: token.oauth_client.client_id }
untenanted do
post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: token.raw_refresh_token }.merge(params), headers: headers
end
end
def authorization_code_for(client)
Oauth::AuthorizationCode.generate \
client_id: client.client_id,
identity_id: identities(:david).id,
code_challenge: Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false),
redirect_uri: "https://connector.example.com/callback",
scope: "read"
end
end