diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 38e7596a..c0ed90dc 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -79,15 +79,19 @@ class Rack::Attack # bad intent. We are assuming good intent and thus provide users with info # about how many requests are allowed in a time period and how to remove # those restrictions by registering. - Rack::Attack.throttled_responder = lambda do |env| - match_data = env['rack.attack.match_data'] - now = match_data[:epoch_time] + # Rack::Attack 6.x calls throttled_responder with a request object. + # Read throttle metadata from request.env rather than from a raw env hash. + Rack::Attack.throttled_responder = lambda do |request| + match_data = request.env['rack.attack.match_data'] || {} + now = match_data[:epoch_time] || Time.now.to_i + period = match_data[:period].to_i + reset = period.positive? ? (now + (period - now % period)) : now headers = { 'Content-Type' => 'application/json', 'RateLimit-Limit' => match_data[:limit].to_s, 'RateLimit-Remaining' => '0', - 'RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s + 'RateLimit-Reset' => reset.to_s } body = { diff --git a/test/initializers/rack_attack_test.rb b/test/initializers/rack_attack_test.rb new file mode 100644 index 00000000..2e60079e --- /dev/null +++ b/test/initializers/rack_attack_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class RackAttackTest < ActiveSupport::TestCase + test 'throttled_responder accepts Rack::Attack::Request and returns 429 response' do + env = { + 'rack.attack.match_data' => { + limit: 100, + count: 101, + period: 60, + epoch_time: 1_700_000_000 + } + } + + request = Rack::Attack::Request.new(env) + + status, headers, body = Rack::Attack.throttled_responder.call(request) + + assert_equal 429, status + assert_equal 'application/json', headers['Content-Type'] + assert_equal '100', headers['RateLimit-Limit'] + assert_equal '0', headers['RateLimit-Remaining'] + assert_equal '1700000040', headers['RateLimit-Reset'] + + parsed_body = JSON.parse(body.first) + assert_equal '100', parsed_body['request_limit'] + assert_equal '101', parsed_body['request_count'] + assert_equal true, parsed_body.key?('error') + end + + test 'safelist allows authenticated request with valid bearer token and user' do + user = users(:yo) + request = Rack::Attack::Request.new('HTTP_AUTHORIZATION' => 'Bearer valid.token') + + JwtWrapper.stubs(:decode).with('valid.token').returns({ 'user_id' => user.id }) + + assert_equal true, Rack::Attack.configuration.safelisted?(request) + end + + test 'safelist rejects invalid bearer token' do + request = Rack::Attack::Request.new('HTTP_AUTHORIZATION' => 'Bearer invalid.token') + + JwtWrapper.stubs(:decode).with('invalid.token').raises(JWT::DecodeError) + + assert_equal false, Rack::Attack.configuration.safelisted?(request) + end + + test 'safelist rejects non-bearer authorization strategy' do + request = Rack::Attack::Request.new('HTTP_AUTHORIZATION' => 'Basic abc123') + + assert_equal false, Rack::Attack.configuration.safelisted?(request) + end +end