Rate limit by user instead of IP when API user is authenticated (#5923)
* Fix #668 - Rate limit by user instead of IP when API user is authenticated * Fix code style issue * Use request decorator provided by Doorkeeper
This commit is contained in:
		
							parent
							
								
									84cebad49d
								
							
						
					
					
						commit
						a865b62efc
					
				@ -44,7 +44,8 @@ module RateLimitHeaders
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def api_throttle_data
 | 
					  def api_throttle_data
 | 
				
			||||||
    request.env['rack.attack.throttle_data']['api']
 | 
					    request.env['rack.attack.throttle_data']['throttle_authenticated_api'] ||
 | 
				
			||||||
 | 
					      request.env['rack.attack.throttle_data']['throttle_unauthenticated_api']
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def request_time
 | 
					  def request_time
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,41 @@
 | 
				
			|||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Rack::Attack
 | 
					class Rack::Attack
 | 
				
			||||||
 | 
					  class Request
 | 
				
			||||||
 | 
					    def authenticated_token
 | 
				
			||||||
 | 
					      return @token if defined?(@token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @token = Doorkeeper::OAuth::Token.authenticate(
 | 
				
			||||||
 | 
					        Doorkeeper::Grape::AuthorizationDecorator.new(self),
 | 
				
			||||||
 | 
					        *Doorkeeper.configuration.access_token_methods
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def authenticated_user_id
 | 
				
			||||||
 | 
					      authenticated_token&.resource_owner_id
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unauthenticated?
 | 
				
			||||||
 | 
					      !authenticated_user_id
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def api_request?
 | 
				
			||||||
 | 
					      path.start_with?('/api')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def web_request?
 | 
				
			||||||
 | 
					      !api_request?
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PROTECTED_PATHS = %w(
 | 
				
			||||||
 | 
					    /auth/sign_in
 | 
				
			||||||
 | 
					    /auth
 | 
				
			||||||
 | 
					    /auth/password
 | 
				
			||||||
 | 
					  ).freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PROTECTED_PATHS_REGEX = Regexp.union(PROTECTED_PATHS.map { |path| /\A#{Regexp.escape(path)}/ })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Always allow requests from localhost
 | 
					  # Always allow requests from localhost
 | 
				
			||||||
  # (blocklist & throttles are skipped)
 | 
					  # (blocklist & throttles are skipped)
 | 
				
			||||||
  Rack::Attack.safelist('allow from localhost') do |req|
 | 
					  Rack::Attack.safelist('allow from localhost') do |req|
 | 
				
			||||||
@ -8,24 +43,16 @@ class Rack::Attack
 | 
				
			|||||||
    '127.0.0.1' == req.ip || '::1' == req.ip
 | 
					    '127.0.0.1' == req.ip || '::1' == req.ip
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Rate limits for the API
 | 
					  throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
 | 
				
			||||||
  throttle('api', limit: 300, period: 5.minutes) do |req|
 | 
					    req.api_request? && req.authenticated_user_id
 | 
				
			||||||
    req.ip if req.path =~ /\A\/api\/v/
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Rate limit logins
 | 
					  throttle('throttle_unauthenticated_api', limit: 300, period: 5.minutes) do |req|
 | 
				
			||||||
  throttle('login', limit: 5, period: 5.minutes) do |req|
 | 
					    req.ip if req.api_request? && req.unauthenticated?
 | 
				
			||||||
    req.ip if req.path == '/auth/sign_in' && req.post?
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Rate limit sign-ups
 | 
					  throttle('protected_paths', limit: 5, period: 5.minutes) do |req|
 | 
				
			||||||
  throttle('register', limit: 5, period: 5.minutes) do |req|
 | 
					    req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
 | 
				
			||||||
    req.ip if req.path == '/auth' && req.post?
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Rate limit forgotten passwords
 | 
					 | 
				
			||||||
  throttle('reminder', limit: 5, period: 5.minutes) do |req|
 | 
					 | 
				
			||||||
    req.ip if req.path == '/auth/password' && req.post?
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  self.throttled_response = lambda do |env|
 | 
					  self.throttled_response = lambda do |env|
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ describe ApplicationController do
 | 
				
			|||||||
      let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc }
 | 
					      let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        request.env['rack.attack.throttle_data'] = { 'api' => { limit: 100, count: 20, period: 10 } }
 | 
					        request.env['rack.attack.throttle_data'] = { 'throttle_authenticated_api' => { limit: 100, count: 20, period: 10 } }
 | 
				
			||||||
        travel_to start_time do
 | 
					        travel_to start_time do
 | 
				
			||||||
          get 'show'
 | 
					          get 'show'
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user