Skip to content

Commit

Permalink
Add rate limiting and reCAPTCHA to resend_auth_email (#803)
Browse files Browse the repository at this point in the history
Fixes issue where sometimes the reCAPTCHA is not shown and user instead sees 🎷
Supports rate limiting and reCAPTCHA in staging and development environments
  • Loading branch information
lgebhardt authored and nvonpentz committed Apr 24, 2018
1 parent 048c341 commit d1fa3e9
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 112 deletions.
14 changes: 6 additions & 8 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ gem "premailer-rails", "~> 1.9.4", require: false
# Puma as app server
gem "puma", "3.10"

# Make cracking a little bit harder
gem "rack-attack", "~> 5.0"

gem "rails", "~> 5.0.0", ">= 5.0.0.1"

# I love captchas
gem "recaptcha", "~> 3.3", require: "recaptcha/rails"

# Cache with Redis
gem "redis-rails", "~> 5"

Expand Down Expand Up @@ -145,14 +151,6 @@ group :development, :test do
gem "chromedriver-helper"
end

group :production, :staging do
# Make cracking a little bit harder
gem "rack-attack", "~> 5.0"

# I love captchas
gem "recaptcha", "~> 3.3", require: "recaptcha/rails"
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ Setup a google API project:
* Update your env to include `TWITCH_CLIENT_SECRET="your-app-secret"`
* Save the app

### reCAPTCHA Setup

In order to test the rate limiting and captcha components you will need to setup an account with Google's
[reCAPTCHA](https://www.google.com/recaptcha/intro/android.html). Instructions can be found at the
[reCAPTCHA gem repo](https://github.com/ambethia/recaptcha#rails-installation). Add the api keys to your Env variables.

### Local Eyeshade Setup

1. Follow the [setup instructions](https://github.com/brave-intl/bat-ledger) for bat-ledger
Expand Down
27 changes: 16 additions & 11 deletions app/controllers/publishers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class PublishersController < ApplicationController
# Number of requests to #create before we present a captcha.
THROTTLE_THRESHOLD_CREATE = 3
THROTTLE_THRESHOLD_CREATE_AUTH_TOKEN = 3
THROTTLE_THRESHOLD_RESEND_AUTH_EMAIL = 3

include PublishersHelper
include PromosHelper
Expand Down Expand Up @@ -58,7 +59,7 @@ def create
@publisher = Publisher.new(pending_email: email)
@publisher_email = @publisher.pending_email

@should_throttle = should_throttle_create? || params[:captcha]
@should_throttle = should_throttle_create? || params[:captcha].present?
throttle_legit =
@should_throttle ?
verify_recaptcha(model: @publisher)
Expand Down Expand Up @@ -95,7 +96,7 @@ def create_done
def resend_auth_email
@publisher = Publisher.find(params[:publisher_id])

@should_throttle = should_throttle_create_auth_token? || params[:captcha]
@should_throttle = should_throttle_resend_auth_email? || params[:captcha].present?
throttle_legit =
@should_throttle ?
verify_recaptcha(model: @publisher)
Expand Down Expand Up @@ -219,7 +220,7 @@ def create_auth_token
end

@publisher = Publisher.new(publisher_create_auth_token_params)
@should_throttle = should_throttle_create_auth_token? || params[:captcha]
@should_throttle = should_throttle_create_auth_token? || params[:captcha].present?
throttle_legit =
@should_throttle ?
verify_recaptcha(model: @publisher)
Expand Down Expand Up @@ -465,17 +466,21 @@ def require_publisher_email_verified_through_youtube_auth
# Level 1 throttling -- After the first two requests, ask user to
# submit a captcha. See rack-attack.rb for throttle keys.
def should_throttle_create?
Rails.env.production? &&
request.env["rack.attack.throttle_data"] &&
request.env["rack.attack.throttle_data"]["registrations/ip"] &&
request.env["rack.attack.throttle_data"]["registrations/ip"][:count] >= THROTTLE_THRESHOLD_CREATE
request.env["rack.attack.throttle_data"] &&
request.env["rack.attack.throttle_data"]["registrations/ip"] &&
request.env["rack.attack.throttle_data"]["registrations/ip"][:count] >= THROTTLE_THRESHOLD_CREATE
end

def should_throttle_create_auth_token?
Rails.env.production? &&
request.env["rack.attack.throttle_data"] &&
request.env["rack.attack.throttle_data"]["created-auth-tokens/ip"] &&
request.env["rack.attack.throttle_data"]["created-auth-tokens/ip"][:count] >= THROTTLE_THRESHOLD_CREATE_AUTH_TOKEN
request.env["rack.attack.throttle_data"] &&
request.env["rack.attack.throttle_data"]["created-auth-tokens/ip"] &&
request.env["rack.attack.throttle_data"]["created-auth-tokens/ip"][:count] >= THROTTLE_THRESHOLD_CREATE_AUTH_TOKEN
end

def should_throttle_resend_auth_email?
request.env["rack.attack.throttle_data"] &&
request.env["rack.attack.throttle_data"]["resend_auth_email/publisher_id"] &&
request.env["rack.attack.throttle_data"]["resend_auth_email/publisher_id"][:count] >= THROTTLE_THRESHOLD_RESEND_AUTH_EMAIL
end

def prompt_for_two_factor_setup
Expand Down
13 changes: 7 additions & 6 deletions app/views/publishers/emailed_auth_token.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
h3.single-panel--headline= t ".heading"

.col-small-centered
- if @should_throttle
.form-group
= recaptcha_tags
= t ".body_html", email: @publisher_email, try_again_link: link_to(t(".try_again"), "#", onclick: "document.getElementById('resend_email_form').submit();")
= form_tag resend_auth_email_publishers_path(publisher_id: @publisher.id), method: "post", class: "hidden", id: "resend_email_form" do
p= t ".sent_access_link_html", email: @publisher_email
hr
= form_tag resend_auth_email_publishers_path(publisher_id: @publisher.id), method: "post", class: "hidden", id: "resend_email_form" do
- if params[:captcha]
= hidden_field_tag :captcha

- if @should_throttle
.col-small-centered
= recaptcha_tags
p= t ".resend_email_html", try_again_link: link_to(t(".try_again"), "#", onclick: "document.getElementById('resend_email_form').submit();")
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
# since you don"t have to restart the web server when you make code changes.
config.cache_classes = false

# Rate limiting
config.middleware.use(Rack::Attack)

# Do not eager load code on boot.
config.eager_load = false

Expand Down
171 changes: 88 additions & 83 deletions config/initializers/rack-attack.rb
Original file line number Diff line number Diff line change
@@ -1,101 +1,106 @@
if Rails.env.production? || Rails.env.staging?
class Rack::Attack

# Safelists
if Rails.application.secrets[:api_ip_whitelist]
API_IP_WHITELIST = Rails.application.secrets[:api_ip_whitelist].split(",").freeze
else
API_IP_WHITELIST = [].freeze
end
class Rack::Attack

safelist('allow/API_IP_WHITELIST') do |req|
# Requests are allowed if the return value is truthy
API_IP_WHITELIST.include?(req.ip)
end
# Safelists
if Rails.application.secrets[:api_ip_whitelist]
API_IP_WHITELIST = Rails.application.secrets[:api_ip_whitelist].split(",").freeze
else
API_IP_WHITELIST = [].freeze
end

safelist('allow/API_IP_WHITELIST') do |req|
# Requests are allowed if the return value is truthy
API_IP_WHITELIST.include?(req.ip)
end

### Configure Cache ###

### Configure Cache ###
# If you don't want to use Rails.cache (Rack::Attack's default), then
# configure it here.
#
# Note: The store is only used for throttling (not blacklisting and
# whitelisting). It must implement .increment and .write like
# ActiveSupport::Cache::Store

# If you don't want to use Rails.cache (Rack::Attack's default), then
# configure it here.
#
# Note: The store is only used for throttling (not blacklisting and
# whitelisting). It must implement .increment and .write like
# ActiveSupport::Cache::Store
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
### Throttle Spammy Clients ###

### Throttle Spammy Clients ###
# If any single client IP is making tons of requests, then they're
# probably malicious or a poorly-configured scraper. Either way, they
# don't deserve to hog all of the app server's CPU. Cut them off!
#
# Note: If you're serving assets through rack, those requests may be
# counted by rack-attack and this throttle may be activated too
# quickly. If so, enable the condition to exclude them from tracking.

# If any single client IP is making tons of requests, then they're
# probably malicious or a poorly-configured scraper. Either way, they
# don't deserve to hog all of the app server's CPU. Cut them off!
#
# Note: If you're serving assets through rack, those requests may be
# counted by rack-attack and this throttle may be activated too
# quickly. If so, enable the condition to exclude them from tracking.
# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
throttle("req/ip", limit: 300, period: 5.minutes) do |req|
req.ip if !req.path.start_with?("/assets")
end

# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
throttle("req/ip", limit: 300, period: 5.minutes) do |req|
req.ip if !req.path.start_with?("/assets")
### Prevent Brute-Force Login Attacks ###

# The most common brute-force login attack is a brute-force password
# attack where an attacker simply tries a large number of emails and
# passwords to see if any credentials match.
#
# Another common method of attack is to use a swarm of computers with
# different IPs to try brute-forcing a password for a specific account.

# Throttle POST requests to /login by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
throttle("logins/ip", limit: 5, period: 120.seconds) do |req|
if req.path.start_with?("/publishers/") && req.params["token"]
req.ip
end
end

### Prevent Brute-Force Login Attacks ###

# The most common brute-force login attack is a brute-force password
# attack where an attacker simply tries a large number of emails and
# passwords to see if any credentials match.
#
# Another common method of attack is to use a swarm of computers with
# different IPs to try brute-forcing a password for a specific account.

# Throttle POST requests to /login by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
throttle("logins/ip", limit: 5, period: 20.seconds) do |req|
if req.path.start_with?("/publishers/") && req.params["token"]
req.ip
end
throttle("created-auth-tokens/ip", limit: 5, period: 20.seconds) do |req|
if req.path == "/publishers/log_in" && req.post?
req.ip
end
end

throttle("created-auth-tokens/ip", limit: 5, period: 20.seconds) do |req|
if req.path == "/publishers/log_in" && req.post?
req.ip
end
# Throttle resend auth emails for a publisher
throttle("resend_auth_email/publisher_id", limit: 5, period: 5.minutes) do |req|
if req.path == "/publishers/resend_auth_email" && req.post?
req['publisher_id']
end
end

# Throttle POST requests to /login by email param
#
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
#
# Note: This creates a problem where a malicious user could intentionally
# throttle logins for another user and force their login requests to be
# denied, but that"s not very common and shouldn"t happen to you. (Knock
# on wood!)
# throttle("logins/email", :limit => 5, :period => 20.seconds) do |req|
# if req.path.start_with?("/publishers/") && req.params["token"]
# return the email if present, nil otherwise
# req.params["email"].presence
# end
# end

# In PublishersController we'll check the annotated request object
# to apply additional Recaptcha.
throttle("registrations/ip", limit: 60, period: 1.hour) do |req|
if req.path == "/publishers" && req.post?
req.ip
end
# Throttle POST requests to /login by email param
#
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
#
# Note: This creates a problem where a malicious user could intentionally
# throttle logins for another user and force their login requests to be
# denied, but that"s not very common and shouldn"t happen to you. (Knock
# on wood!)
# throttle("logins/email", :limit => 5, :period => 20.seconds) do |req|
# if req.path.start_with?("/publishers/") && req.params["token"]
# return the email if present, nil otherwise
# req.params["email"].presence
# end
# end

# In PublishersController we'll check the annotated request object
# to apply additional Recaptcha.
throttle("registrations/ip", limit: 60, period: 1.hour) do |req|
if req.path == "/publishers" && req.post?
req.ip
end
end


### Custom Throttle Response ###
self.throttled_response = lambda do |env|
[
420, # status
{"Content-Type" => "text/plain; charset=UTF-8"}, # headers
["🎷"] # body
]
end
### Custom Throttle Response ###
self.throttled_response = lambda do |env|
[
420, # status
{"Content-Type" => "text/plain; charset=UTF-8"}, # headers
["🎷"] # body
]
end
end
6 changes: 2 additions & 4 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,8 @@ en:
heading: Login email sent!
body: Please check your email for the login link.
heading: An email is on its way!
body_html: |
<p>We just sent an access link to <strong class="email-address">%{email}</strong>. Click on the link to log in to your account</p>
<hr>
<p>Don't see the email? Be sure to check your spam folder. Please wait for a few minutes and %{try_again_link}.</p>
sent_access_link_html: We just sent an access link to <strong class="email-address">%{email}</strong>. Click on the link to log in to your account
resend_email_html: Don't see the email? Be sure to check your spam folder. Please wait for a few minutes and %{try_again_link}.
try_again: try again
devise:
login_session_expired: Your session has expired. Please log in again.
Expand Down
1 change: 1 addition & 0 deletions docs/publishers-secrets.example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export API_EYESHADE_OFFLINE=1
export INTERNAL_EMAIL="admin@publishers.local" # Admin notifications get sent here.
#export LOG_API_REQUESTS=1 # Enable to log publishers' external API access.
#export MAILER_SENDER="" # The From: header in emails sent to users.
export REDIS_URL="redis://127.0.0.1:6379/0"
export RECAPTCHA_PUBLIC_KEY="" # For recaptcha for rate limiting.
export RECAPTCHA_PRIVATE_KEY=""
#export SENTRY_DSN="" # Exception handling
Expand Down

0 comments on commit d1fa3e9

Please sign in to comment.