Skip to content

Commit 0c4a68d

Browse files
committed
--no-edit
1 parent 775ed13 commit 0c4a68d

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

config/config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ RATE_LIMIT_NUMBER: 300
189189
RATE_LIMIT_PERIOD: 300
190190
RATE_LIMIT_LOGIN_ATTEMPTS: 5
191191
RATE_LIMIT_LOGIN_PERIOD: 20
192+
RATE_LIMIT_ADMIN_LOGIN_ATTEMPTS: 10
193+
RATE_LIMIT_ADMIN_LOGIN_PERIOD: 300
192194
RATE_LIMIT_PER_NGINX_UPSTREAM:
193195
unicorn_elastic:
194196
limit: 300

config/initializers/rack_attack.rb

+18
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ 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+
admin_login_limit = ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_ATTEMPTS
86+
admin_login_period = ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_PERIOD
87+
88+
# Throttle POST requests to /admin/login by IP address
89+
#
90+
# Key: "rack::attack:#{Time.now.to_i/:period}:admin_logins/ip:#{req.ip}"
91+
throttle("admin_logins/ip", limit: admin_login_limit, period: admin_login_period) do |req|
92+
req.ip if req.path == "/admin/login" && req.post?
93+
end
94+
95+
# Throttle POST requests to /admin/login by login param (user name or email)
96+
#
97+
# Key: "rack::attack:#{Time.now.to_i/:period}:admin_logins/email:#{login}"
98+
throttle("admin_logins/email", limit: admin_login_limit, period: admin_login_period) do |req|
99+
req.params.dig("admin", "login").presence if req.path == "/admin/login" && req.post?
100+
end
101+
84102
# Add Retry-After response header to let polite clients know
85103
# how many seconds they should wait before trying again
86104
Rack::Attack.throttled_response_retry_after_header = true

spec/requests/rack_attack_spec.rb

+77
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: "secretpassword" } }
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
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+
ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_ATTEMPTS.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 <= ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_PERIOD.seconds
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 ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_PERIOD.seconds
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+
ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_ATTEMPTS.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 <= ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_PERIOD.seconds
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 ArchiveConfig.RATE_LIMIT_ADMIN_LOGIN_PERIOD.seconds
189+
post admin_session_path, params: params, env: unique_ip_env
190+
expect(response).to have_http_status(:ok)
191+
end
192+
end
116193
end

0 commit comments

Comments
 (0)