Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate limit by user instead of IP when API user is authenticated #5923

Merged
merged 3 commits into from Dec 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/controllers/concerns/rate_limit_headers.rb
Expand Up @@ -44,7 +44,8 @@ def rate_limit_reset
end

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

def request_time
Expand Down
55 changes: 41 additions & 14 deletions config/initializers/rack_attack.rb
@@ -1,31 +1,58 @@
# frozen_string_literal: true

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
# (blocklist & throttles are skipped)
Rack::Attack.safelist('allow from localhost') do |req|
# Requests are allowed if the return value is truthy
'127.0.0.1' == req.ip || '::1' == req.ip
end

# Rate limits for the API
throttle('api', limit: 300, period: 5.minutes) do |req|
req.ip if req.path =~ /\A\/api\/v/
end

# Rate limit logins
throttle('login', limit: 5, period: 5.minutes) do |req|
req.ip if req.path == '/auth/sign_in' && req.post?
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
req.api_request? && req.authenticated_user_id
end

# Rate limit sign-ups
throttle('register', limit: 5, period: 5.minutes) do |req|
req.ip if req.path == '/auth' && req.post?
throttle('throttle_unauthenticated_api', limit: 300, period: 5.minutes) do |req|
req.ip if req.api_request? && req.unauthenticated?
end

# Rate limit forgotten passwords
throttle('reminder', limit: 5, period: 5.minutes) do |req|
req.ip if req.path == '/auth/password' && req.post?
throttle('protected_paths', limit: 5, period: 5.minutes) do |req|
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
end

self.throttled_response = lambda do |env|
Expand Down
2 changes: 1 addition & 1 deletion spec/controllers/concerns/rate_limit_headers_spec.rb
Expand Up @@ -34,7 +34,7 @@ def show
let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc }

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
get 'show'
end
Expand Down