test/integration/oauth_availability_test.rb
Rama main · réplica de lectura del repositorio original.
require "test_helper"
class OauthAvailabilityTest < ActionDispatch::IntegrationTest
include OauthAvailabilityTestHelper
include OauthClientCredentialsTestHelper
CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
# Discovery follows acceptance
test "a dark server publishes no discovery documents" do
with_oauth_availability acceptance: false do
untenanted { get "/.well-known/oauth-authorization-server" }
assert_response :not_found
untenanted { get "/.well-known/oauth-protected-resource" }
assert_response :not_found
end
end
test "a pilot client does not make a dark server discoverable" do
with_oauth_availability acceptance: false, pilot_client_ids: [ oauth_clients(:mcp_client).client_id ] do
untenanted { get "/.well-known/oauth-authorization-server", params: { client_id: oauth_clients(:mcp_client).client_id } }
assert_response :not_found
end
end
test "pausing issuance keeps discovery up" do
with_oauth_availability acceptance: true, issuance: false do
untenanted { get "/.well-known/oauth-authorization-server" }
assert_response :success
untenanted { get "/.well-known/oauth-protected-resource" }
assert_response :success
end
end
# Issuance
test "a dark server registers no clients" do
with_oauth_availability acceptance: false do
assert_no_difference "Oauth::Client.count" do
untenanted { post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/callback" ] }, as: :json }
end
end
assert_response :not_found
end
test "a dark server answers 404 even past the rate limit" do
ActiveSupport::Cache::NullStore.any_instance.stubs(:increment).returns(1_000)
with_oauth_availability acceptance: false do
untenanted { post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/callback" ] }, as: :json }
assert_response :not_found
untenanted { post oauth_token_path, params: { grant_type: "authorization_code", code: "x" } }
assert_response :not_found
end
end
test "naming a pilot client does not open registration on a dark server" do
with_oauth_availability acceptance: false, issuance: false, pilot_client_ids: [ oauth_clients(:mcp_client).client_id ] do
assert_no_difference "Oauth::Client.count" do
untenanted { post oauth_clients_path, params: { client_id: oauth_clients(:mcp_client).client_id, redirect_uris: [ "http://127.0.0.1:9999/callback" ] }, as: :json }
end
end
assert_response :not_found
end
test "pausing issuance refuses registration with 503" do
with_oauth_availability acceptance: true, issuance: false do
assert_no_difference "Oauth::Client.count" do
untenanted { post oauth_clients_path, params: { redirect_uris: [ "http://127.0.0.1:9999/callback" ] }, as: :json }
end
end
assert_response :service_unavailable
end
test "a dark server does not ask a signed-out user to sign in to authorize" do
with_oauth_availability acceptance: false do
untenanted { get new_oauth_authorization_path, params: authorization_params }
end
assert_response :not_found
end
test "a dark server shows no consent screen" do
sign_in_as :david
with_oauth_availability acceptance: false do
untenanted { get new_oauth_authorization_path, params: authorization_params }
end
assert_response :not_found
end
test "a dark server issues no code on approval" do
sign_in_as :david
with_oauth_availability acceptance: false do
untenanted { post oauth_authorization_path, params: authorization_params }
end
assert_response :not_found
end
test "pausing issuance refuses authorization with 503" do
sign_in_as :david
with_oauth_availability acceptance: true, issuance: false do
untenanted { get new_oauth_authorization_path, params: authorization_params }
assert_response :service_unavailable
untenanted { post oauth_authorization_path, params: authorization_params }
assert_response :service_unavailable
end
end
test "a denial still reaches the client while dark" do
sign_in_as :david
with_oauth_availability acceptance: false, issuance: false do
untenanted { post oauth_authorization_path, params: authorization_params.merge(error: "access_denied") }
end
assert_response :redirect
assert_match %r{\Ahttp://127\.0\.0\.1:8888/callback\?error=access_denied}, response.location
end
test "a dark server mints no tokens" do
with_oauth_availability acceptance: false do
assert_no_difference "Identity::AccessToken.count" do
untenanted { post oauth_token_path, params: token_params, as: :json }
end
end
assert_response :not_found
end
test "pausing issuance refuses the token endpoint with 503" do
with_oauth_availability acceptance: true, issuance: false do
assert_no_difference "Identity::AccessToken.count" do
untenanted { post oauth_token_path, params: token_params, as: :json }
end
end
assert_response :service_unavailable
end
test "a pilot client completes the flow against a dark server" do
sign_in_as :david
client = oauth_clients(:mcp_client)
with_oauth_availability acceptance: false, issuance: false, pilot_client_ids: [ client.client_id ] do
untenanted { get new_oauth_authorization_path, params: authorization_params }
assert_response :success
untenanted { post oauth_authorization_path, params: authorization_params }
assert_response :redirect
untenanted { post oauth_token_path, params: token_params, as: :json }
assert_response :success
get user_path(users(:david)), env: bearer(response.parsed_body["access_token"]), as: :json
assert_response :success
end
end
test "another client is still refused while a pilot runs" do
with_oauth_availability acceptance: false, pilot_client_ids: [ oauth_clients(:trusted_client).client_id ] do
untenanted { post oauth_token_path, params: token_params, as: :json }
end
assert_response :not_found
end
test "a dark server refreshes no grants" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client), permission: :read)
with_oauth_availability acceptance: false do
untenanted { post oauth_token_path, params: refresh_params(token), as: :json }
end
assert_response :not_found
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "pausing issuance refuses refresh with 503 and leaves the grant untouched" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client), permission: :read)
raw_token = token.raw_token
with_oauth_availability acceptance: true, issuance: false do
untenanted { post oauth_token_path, params: refresh_params(token), as: :json }
assert_response :service_unavailable
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
get user_path(users(:david)), env: bearer(raw_token), as: :json
assert_response :success
end
end
test "a pilot client refreshes against a dark server" do
client = oauth_clients(:mcp_client)
token = identities(:david).access_tokens.create!(oauth_client: client, permission: :read)
with_oauth_availability acceptance: false, issuance: false, pilot_client_ids: [ client.client_id ] do
untenanted { post oauth_token_path, params: refresh_params(token), as: :json }
end
assert_response :success
assert_not_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a dark server refreshes no confidential grants, even with the right secret" do
client = oauth_clients(:confidential_client)
token = identities(:david).access_tokens.create!(oauth_client: client, permission: :read)
with_oauth_availability acceptance: false do
untenanted { post oauth_token_path, params: refresh_params(token).merge(client_secret: "confidential_secret_789") }
end
assert_response :not_found
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
test "a confidential pilot client still authenticates against a dark server" do
client = oauth_clients(:confidential_client)
token = identities(:david).access_tokens.create!(oauth_client: client, permission: :read)
with_oauth_availability acceptance: false, issuance: false, pilot_client_ids: [ client.client_id ] do
untenanted { post oauth_token_path, params: refresh_params(token).merge(client_secret: "wrong") }
end
assert_client_authentication_failed basic: false
assert_equal token.refresh_token_digest, token.reload.refresh_token_digest
end
# Acceptance
test "OAuth bearer tokens are refused while acceptance is off" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client), permission: :read)
with_oauth_availability acceptance: false do
get user_path(users(:david)), env: bearer(token.raw_token), as: :json
end
assert_response :unauthorized
end
test "personal access tokens still work while OAuth is dark" do
with_oauth_availability acceptance: false do
get user_path(users(:jason)), env: bearer(identity_access_tokens(:jasons_api_token).token), as: :json
end
assert_response :success
end
test "pausing issuance keeps existing OAuth tokens working" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client), permission: :read)
with_oauth_availability acceptance: true, issuance: false do
get user_path(users(:david)), env: bearer(token.raw_token), as: :json
end
assert_response :success
end
# Management is never gated
test "revocation works while dark" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client), permission: :read)
with_oauth_availability acceptance: false, issuance: false do
assert_difference "Identity::AccessToken.count", -1 do
untenanted { post oauth_revocation_path, params: { token: token.raw_token, client_id: token.oauth_client.client_id } }
end
end
assert_response :success
end
test "Connected Apps lists and disconnects while dark" do
sign_in_as :david
client = oauth_clients(:mcp_client)
identities(:david).access_tokens.create!(oauth_client: client, permission: :read)
with_oauth_availability acceptance: false, issuance: false do
get my_connected_apps_path
assert_response :success
assert_match client.name, response.body
assert_difference "Identity::AccessToken.count", -1 do
delete my_connected_app_path(client)
end
assert_redirected_to my_connected_apps_path
end
end
private
def authorization_params
{
client_id: oauth_clients(:mcp_client).client_id,
redirect_uri: "http://127.0.0.1:8888/callback",
response_type: "code",
code_challenge: Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false),
code_challenge_method: "S256",
scope: "read",
state: "xyz123"
}
end
def token_params
{
grant_type: "authorization_code",
client_id: oauth_clients(:mcp_client).client_id,
code: Oauth::AuthorizationCode.generate(
client_id: oauth_clients(:mcp_client).client_id,
identity_id: identities(:david).id,
code_challenge: authorization_params[:code_challenge],
redirect_uri: "http://127.0.0.1:8888/callback",
scope: "read"),
redirect_uri: "http://127.0.0.1:8888/callback",
code_verifier: CODE_VERIFIER
}
end
def refresh_params(token)
{ grant_type: "refresh_token", refresh_token: token.raw_refresh_token, client_id: token.oauth_client.client_id }
end
def bearer(token)
{ "HTTP_AUTHORIZATION" => "Bearer #{token}" }
end
end