Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/controllers/password_resets_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
class PasswordResetsController < ApplicationController
allow_unauthenticated_access
rate_limit to: 3,
within: 1.minute, with: -> {
flash.now[:alert] = t("controllers.password_resets.update.rate_limit")
render :new, status: :too_many_requests
},
only: [:create], by: -> { request.remote_ip }

before_action :set_user_by_token, only: [:edit, :update]

Expand Down
7 changes: 7 additions & 0 deletions app/controllers/user_sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
class UserSessionsController < ApplicationController
allow_unauthenticated_access only: [:new, :create]
rate_limit to: 3,
within: 1.minute, with: -> {
flash.now[:alert] = t("controllers.user_sessions.create.rate_limit")
@user = User.new
render :new, status: :too_many_requests
},
only: [:create], by: -> { request.remote_ip }

before_action :redirect_if_signed_in, only: [:new, :create]

Expand Down
2 changes: 1 addition & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really test rate limiting without having this set unfortunately, so if there are any other suggestions I am open to it.

config.cache_store = :memory_store

# Render exception templates for rescuable exceptions and raise for other exceptions.
config.action_dispatch.show_exceptions = :rescuable
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ en:
notice: "Check your email to reset your password."
update:
notice: "Your password has been reset successfully. Please login."
rate_limit: "Please try again later."
errors:
invalid_token: "Invalid token, please try again."
passwords:
Expand All @@ -28,6 +29,7 @@ en:
create:
notice: "Signed in successfully."
alert: "Invalid email or password."
rate_limit: "Please try again later."
destroy:
notice: "You have been logged out."
users:
Expand Down
39 changes: 39 additions & 0 deletions spec/controllers/password_resets_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,45 @@
expect(response).to redirect_to(post_submit_password_reset_path)
end
end

context "with rate limit" do
before do
Rails.application.config.action_controller.perform_caching = true
Rails.cache.clear
end

after do
Rails.cache.clear
end

let(:params) do
{
email: user.email
}
end

it "allows up to 3 requests within 1 minute" do
3.times do |i|
request.env["REMOTE_ADDR"] = "192.168.1.100"
post :create, params: params
expect(response).to redirect_to(post_submit_password_reset_path)
end
end

it "blocks the 4th request" do
3.times do
request.env["REMOTE_ADDR"] = "192.168.1.100"
post :create, params: params
expect(response).to redirect_to(post_submit_password_reset_path)
end

request.env["REMOTE_ADDR"] = "192.168.1.100"
post :create, params: params

expect(flash[:alert]).to eq("Please try again later.")
expect(response).to have_http_status(:too_many_requests)
end
end
end

describe "GET #edit" do
Expand Down
30 changes: 30 additions & 0 deletions spec/controllers/user_sessions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,35 @@
expect(response).to redirect_to(new_user_session_path)
end
end

context "with rate limit" do
before do
Rails.application.config.action_controller.perform_caching = true
Rails.cache.clear
end

after do
Rails.cache.clear
end

let(:params) do
{
user: {
email: user.email,
password: "invalid_password"
}
}
end

it "rate limits a request after 3 attempts under a minute" do
3.times do
expect { post :create, params: params }.not_to change(User, :count)
expect(response).to have_http_status(:redirect)
end

post :create, params: params
expect(response).to have_http_status(:too_many_requests)
end
end
end
end
2 changes: 2 additions & 0 deletions spec/support/authentication_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module AuthenticationHelper
def sign_in(user, password = "password2024")
if respond_to?(:visit) # System specs
Rails.cache.clear if Rails.cache.respond_to?(:clear)

visit new_user_session_path
find_dti("email_field").set(user.email)
find_dti("password_field").set(password)
Expand Down