test/integration/oauth_grant_idle_expiry_test.rb
Rama main · réplica de lectura del repositorio original.
require "test_helper"
# A grant lapses once its refresh token goes unused for the idle window, as
# bc3's refresh tokens do: each refresh restarts the clock, and a lapsed grant
# takes a fresh authorization, with consent.
class OauthGrantIdleExpiryTest < ActionDispatch::IntegrationTest
setup do
@client = oauth_clients(:mcp_client)
freeze_time
@grant = identities(:david).access_tokens.create!(oauth_client: @client, permission: :read)
end
test "a grant refreshes up to the end of the idle window" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT
refresh @grant.raw_refresh_token
assert_response :success
end
test "a grant idle past the window lapses" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
refresh @grant.raw_refresh_token
assert_response :bad_request
assert_equal "invalid_grant", response.parsed_body["error"]
end
test "each refresh restarts the idle clock" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
refresh @grant.raw_refresh_token
assert_response :success
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
refresh response.parsed_body["refresh_token"]
assert_response :success
end
test "a lapsed grant leaves Connected Apps" do
sign_in_as :david
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
get my_connected_apps_path
assert_response :success
assert_no_match @client.name, response.body
end
test "the sweep removes lapsed grants and keeps live ones" do
live = identities(:david).access_tokens.create!(oauth_client: @client, permission: :read)
personal = identities(:david).access_tokens.create!(permission: :read, description: "PAT")
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
live.refresh
later_by 1.day + 1.second
Identity::AccessToken.cleanup
assert_not Identity::AccessToken.exists?(@grant.id)
assert Identity::AccessToken.exists?(live.id)
assert Identity::AccessToken.exists?(personal.id)
end
test "the sweep spares a grant renewed after it was selected" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
# The sweep holds the grant as it was read when selected, before the renewal.
selection = stub
selection.stubs(:find_each).yields(Identity::AccessToken.find(@grant.id))
Identity::AccessToken.stubs(:lapsed).returns(selection)
Identity::AccessToken.where(id: @grant.id).update_all(updated_at: Time.current)
Identity::AccessToken.cleanup
assert Identity::AccessToken.exists?(@grant.id)
end
test "rotation itself refuses a lapsed grant, so one racing the deadline can't renew it" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
presented = @grant.raw_refresh_token
assert_nil @grant.refresh
assert_equal TokenDigester.digest(presented), @grant.reload.refresh_token_digest
end
test "the idle clock is the grant's last rotation, so a rotation by older code mid-deploy restarts it too" do
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
rotated_by_older_code = Identity::AccessToken.generate_unique_secure_token
Identity::AccessToken.where(id: @grant.id).update_all(refresh_token_digest: TokenDigester.digest(rotated_by_older_code), updated_at: Time.current)
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
refresh rotated_by_older_code
assert_response :success
end
test "a lapsed grant takes its entry point off the profile page too" do
sign_in_as :david
get user_path(users(:david))
assert_match my_connected_apps_path, response.body
later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
get user_path(users(:david))
assert_no_match my_connected_apps_path, response.body
end
private
# Elapsed time in the app's zone, as the model counts it: travel would add
# calendar days in the local zone, an hour off across a DST change.
def later_by(duration)
travel_to Time.current + duration
end
def refresh(refresh_token)
untenanted do
post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: refresh_token, client_id: @client.client_id }
end
end
end