test/integration/oauth_connected_apps_preview_test.rb
Rama main · réplica de lectura del repositorio original.
require "test_helper"
# While connecting apps is in preview (Oauth::Availability), only previewers,
# 37signals staff and the reviewers listed by identity id, can connect an app
# or use what they connected, whichever client asks.
class OauthConnectedAppsPreviewTest < ActionDispatch::IntegrationTest
include OauthAvailabilityTestHelper
CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
test "staff see the consent screen and connect" do
with_oauth_availability preview: true do
sign_in_as :david
get_consent_screen
assert_response :success
assert_select "form[action$=?]", "/oauth/authorization"
post_consent
assert_response :redirect
assert_match %r{\Ahttp://127\.0\.0\.1:8888/callback\?code=}, response.location
end
end
test "anyone else is told connecting apps is in preview, and gets no code" do
with_oauth_availability preview: true do
sign_in_as :jz
get_consent_screen
assert_response :forbidden
assert_match "Connecting apps to Fizzy is in preview", response.body
post_consent
assert_response :forbidden
assert_nil response.location
end
end
test "anyone else can still decline" do
with_oauth_availability preview: true do
sign_in_as :jz
post_consent error: "access_denied"
assert_response :redirect
assert_match "error=access_denied", response.location
end
end
test "everyone connects when it's off" do
sign_in_as :jz
get_consent_screen
assert_response :success
end
test "a code issued to someone no longer staff mints nothing" do
code = authorization_code_for(identities(:david))
identities(:david).update!(staff: false)
with_oauth_availability preview: true do
redeem code
end
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
end
test "a code replayed once its owner isn't staff still revokes the grant it minted" do
code = authorization_code_for(identities(:david))
with_oauth_availability preview: true do
redeem code
assert_response :success
grant = Identity::AccessToken.find_by_raw_token(response.parsed_body["access_token"])
identities(:david).update!(staff: false)
redeem code
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
assert_not Identity::AccessToken.exists?(grant.id), "a replay revokes the grant whoever owns it"
end
end
test "anyone else asking for consent as JSON is refused as JSON" do
with_oauth_availability preview: true do
sign_in_as :jz
untenanted { get new_oauth_authorization_path(format: :json), params: consent_params }
assert_response :forbidden
assert_equal "access_denied", response.parsed_body["error"]
assert_equal "Connecting apps to Fizzy is in preview for now", response.parsed_body["error_description"]
end
end
test "a staff code redeems" do
with_oauth_availability preview: true do
redeem authorization_code_for(identities(:david))
end
assert_response :success
end
test "a grant stops working, and refreshing, once its owner isn't staff" do
grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))
with_oauth_availability preview: true do
get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
assert_response :success
identities(:david).update!(staff: false)
get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
assert_response :unauthorized
assert_match 'error="invalid_token"', response.headers["WWW-Authenticate"]
refresh grant
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
end
assert grant.reload.persisted?, "a refused refresh leaves the grant to its owner to revoke"
end
test "a replayed refresh token still revokes the grant once its owner isn't staff" do
grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))
presented = grant.raw_refresh_token
with_oauth_availability preview: true do
refresh grant
assert_response :success
identities(:david).update!(staff: false)
travel Oauth::RetiredRefreshToken::GRACE + 1.second
untenanted do
post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: presented, client_id: oauth_clients(:mcp_client).client_id }, as: :json
end
assert_response :bad_request
end
assert_not Identity::AccessToken.exists?(grant.id), "a replay revokes the grant whoever owns it"
end
test "a staff grant refreshes" do
grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))
with_oauth_availability preview: true do
refresh grant
end
assert_response :success
end
test "a listed reviewer sees the consent screen and connects, like staff" do
with_oauth_availability preview: true, reviewer_ids: [ identities(:jz).id ] do
sign_in_as :jz
get_consent_screen
assert_response :success
post_consent
assert_response :redirect
assert_match %r{\Ahttp://127\.0\.0\.1:8888/callback\?code=}, response.location
end
end
test "a listed reviewer's code redeems, and the grant works and refreshes, until they're delisted" do
code = authorization_code_for(identities(:jz))
with_oauth_availability preview: true, reviewer_ids: [ identities(:jz).id ] do
redeem code
assert_response :success
access_token, refresh_token = response.parsed_body.values_at("access_token", "refresh_token")
get user_path(users(:jz)), env: { "HTTP_AUTHORIZATION" => "Bearer #{access_token}" }, as: :json
assert_response :success
untenanted do
post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: refresh_token, client_id: oauth_clients(:mcp_client).client_id }, as: :json
end
assert_response :success
end
grant = identities(:jz).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))
with_oauth_availability preview: true, reviewer_ids: [ identities(:mike).id ] do
get user_path(users(:jz)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
assert_response :unauthorized
refresh grant
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
redeem authorization_code_for(identities(:jz))
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
sign_in_as :jz
get_consent_screen
assert_response :forbidden
end
end
test "a reviewer list naming someone else admits no one beyond staff" do
with_oauth_availability preview: true, reviewer_ids: [ identities(:mike).id ] do
sign_in_as :jz
get_consent_screen
assert_response :forbidden
end
end
test "personal access tokens work for anyone" do
identities(:david).update!(staff: false)
with_oauth_availability preview: true do
get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" }, as: :json
end
assert_response :success
end
test "personal access tokens issued by the first-party client work for anyone" do
token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:personal_access), permission: :read, description: "Script")
identities(:david).update!(staff: false)
with_oauth_availability preview: true do
get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{token.raw_token}" }, as: :json
end
assert_response :success
end
private
def get_consent_screen(**params)
untenanted { get new_oauth_authorization_path, params: consent_params(**params) }
end
def post_consent(**params)
untenanted { post oauth_authorization_path, params: consent_params(**params) }
end
def consent_params(**overrides)
{
client_id: oauth_clients(:mcp_client).client_id,
redirect_uri: "http://127.0.0.1:8888/callback",
response_type: "code",
code_challenge: code_challenge,
code_challenge_method: "S256",
scope: "read",
state: "xyz123"
}.merge(overrides)
end
def code_challenge
Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false)
end
def authorization_code_for(identity)
Oauth::AuthorizationCode.generate \
client_id: oauth_clients(:mcp_client).client_id,
identity_id: identity.id,
code_challenge: code_challenge,
redirect_uri: "http://127.0.0.1:8888/callback",
scope: "read"
end
def redeem(code)
untenanted do
post oauth_token_path, params: {
grant_type: "authorization_code",
client_id: oauth_clients(:mcp_client).client_id,
code: code,
redirect_uri: "http://127.0.0.1:8888/callback",
code_verifier: CODE_VERIFIER
}, as: :json
end
end
def refresh(grant)
untenanted do
post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: grant.raw_refresh_token, client_id: oauth_clients(:mcp_client).client_id }, as: :json
end
end
end