Skip to content

Commit 4d91c09

Browse files
committed
Rate limit admin login requests
1 parent 775ed13 commit 4d91c09

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

config/initializers/rack_attack.rb

+19
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,25 @@ class Rack::Attack
8181
req.params.dig("user", "login").presence if req.path == "/users/login" && req.post?
8282
end
8383

84+
### Add Rate Limits for Admin Login ###
85+
86+
admin_login_limit = Rails.application.config.x.rate_limits[:admin_login][:max_attempts]
87+
admin_login_period = Rails.application.config.x.rate_limits[:admin_login][:period]
88+
89+
# Throttle POST requests to /admin/login by IP address
90+
#
91+
# Key: "rack::attack:#{Time.now.to_i/:period}:admin_logins/ip:#{req.ip}"
92+
throttle("admin_logins/ip", limit: admin_login_limit, period: admin_login_period) do |req|
93+
req.ip if req.path == "/admin/login" && req.post?
94+
end
95+
96+
# Throttle POST requests to /admin/login by login param (user name or email)
97+
#
98+
# Key: "rack::attack:#{Time.now.to_i/:period}:admin_logins/email:#{login}"
99+
throttle("admin_logins/email", limit: admin_login_limit, period: admin_login_period) do |req|
100+
req.params.dig("admin", "login").presence if req.path == "/admin/login" && req.post?
101+
end
102+
84103
# Add Retry-After response header to let polite clients know
85104
# how many seconds they should wait before trying again
86105
Rack::Attack.throttled_response_retry_after_header = true

spec/requests/rack_attack_spec.rb

+78-1
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,24 @@ def unique_user_params
1818
{ user: { login: generate(:login), password: "secret" } }
1919
end
2020

21+
def unique_admin_params
22+
{ admin: { login: generate(:login), password: "secret" } }
23+
end
24+
2125
it "test utility returns valid parameters for successful user login attempts" do
2226
params = unique_user_params
2327
create(:user, login: params[:user][:login], password: params[:user][:password])
2428
post user_session_path, params: params.to_query
2529
expect(response).to have_http_status(:redirect)
2630
end
2731

32+
it "test utility returns valid parameters for successful admin login attempts" do
33+
params = unique_admin_params
34+
create(:admin, login: params[:admin][:login], password: params[:admin][:password])
35+
post admin_session_path, params: params.to_query
36+
expect(response).to have_http_status(:redirect)
37+
end
38+
2839
it "successful response does not include retry-after header" do
2940
get root_path, env: { "REMOTE_ADDR" => Faker::Internet.unique.public_ip_v4_address }
3041
expect(response).to have_http_status(:ok)
@@ -113,4 +124,70 @@ def unique_user_params
113124
expect(response).to have_http_status(:ok)
114125
end
115126
end
116-
end
127+
128+
context "when there have been max admin login attempts from an IP address" do
129+
let(:ip) { Faker::Internet.unique.public_ip_v4_address }
130+
131+
before do
132+
10.times do
133+
post admin_session_path, params: unique_admin_params.to_query, env: { "REMOTE_ADDR" => ip }
134+
end
135+
end
136+
137+
it "response to the next attempt from the same IP includes retry-after header" do
138+
post admin_session_path, params: unique_admin_params.to_query, env: { "REMOTE_ADDR" => ip }
139+
expect(response).to have_http_status(:too_many_requests)
140+
expect(response.headers["Retry-After"].to_i).to be > 0
141+
expect(response.headers["Retry-After"].to_i).to be <= 5.minutes
142+
end
143+
144+
it "throttles the next attempt from the same IP" do
145+
post admin_session_path, params: unique_admin_params.to_query, env: { "REMOTE_ADDR" => ip }
146+
expect(response).to have_http_status(:too_many_requests)
147+
end
148+
149+
it "does not throttle an attempt from a different IP" do
150+
post admin_session_path, params: unique_admin_params.to_query, env: unique_ip_env
151+
expect(response).to have_http_status(:ok)
152+
end
153+
154+
it "does not throttle the next attempt from the same IP after some time" do
155+
travel 5.minutes
156+
post admin_session_path, params: unique_admin_params.to_query, env: { "REMOTE_ADDR" => ip }
157+
expect(response).to have_http_status(:ok)
158+
end
159+
end
160+
161+
context "when there have been max admin login attempts for a username" do
162+
let(:params) { unique_admin_params.to_query }
163+
164+
before do
165+
10.times do
166+
post admin_session_path, params: params, env: unique_ip_env
167+
end
168+
end
169+
170+
it "response to the next attempt for the same username includes retry-after header" do
171+
post admin_session_path, params: params, env: unique_ip_env
172+
expect(response).to have_http_status(:too_many_requests)
173+
expect(response.headers["Retry-After"].to_i).to be > 0
174+
expect(response.headers["Retry-After"].to_i).to be <= 5.minutes
175+
end
176+
177+
it "throttles the next attempt for the same username" do
178+
post admin_session_path, params: params, env: unique_ip_env
179+
expect(response).to have_http_status(:too_many_requests)
180+
end
181+
182+
it "does not throttle an attempt for a different username" do
183+
post admin_session_path, params: unique_admin_params.to_query, env: unique_ip_env
184+
expect(response).to have_http_status(:ok)
185+
end
186+
187+
it "does not throttle the next attempt for the same username after some time" do
188+
travel 5.minutes
189+
post admin_session_path, params: params, env: unique_ip_env
190+
expect(response).to have_http_status(:ok)
191+
end
192+
end
193+
end

0 commit comments

Comments
 (0)