Skip to content

Commit

Permalink
Proof-of-concept: reCAPTCHA at sign-in
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed May 10, 2024
1 parent ce2ba86 commit 2dd7c9c
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/components/captcha_submit_button_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
type: :submit,
big: true,
wide: true,
**button_options,
).with_content(content) %>
<% if recaptcha_script_src.present? %>
<%= content_tag(:script, '', src: recaptcha_script_src, async: true) %>
Expand Down
5 changes: 3 additions & 2 deletions app/components/captcha_submit_button_component.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# frozen_string_literal: true

class CaptchaSubmitButtonComponent < BaseComponent
attr_reader :form, :action, :tag_options
attr_reader :form, :action, :button_options, :tag_options

alias_method :f, :form

# @param [String] action https://developers.google.com/recaptcha/docs/v3#actions
def initialize(form:, action:, **tag_options)
def initialize(form:, action:, button_options:, **tag_options)
@form = form
@action = action
@button_options = button_options
@tag_options = tag_options
end

Expand Down
35 changes: 35 additions & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def create
session[:sign_in_flow] = :sign_in
return process_locked_out_session if session_bad_password_count_max_exceeded?
return process_locked_out_user if current_user && user_locked_out?(current_user)
return process_failed_captcha if !valid_captcha_result?

rate_limit_password_failure = true
self.resource = warden.authenticate!(auth_options)
Expand Down Expand Up @@ -79,6 +80,40 @@ def process_locked_out_session
redirect_to root_url
end

def valid_captcha_result?
if cookies[:device] && (device = Device.find_by(cookie_uuid: cookies[:device]))
return true if device.user.email_addresses.lazy.map(&:email).include?(auth_params[:email])
end

response, _assessment_id = recaptcha_form.submit(params.require(:user)[:recaptcha_token])
flash[:error] = response.first_error_message if !response.success?
response.success?
end

def process_failed_captcha
warden.logout(:user)
warden.lock!
redirect_to root_url
end

def recaptcha_form
@recaptcha_form ||= SignInRecaptchaForm.new(**recaptcha_form_args)
end

def recaptcha_form_args
args = { analytics: }
if IdentityConfig.store.phone_recaptcha_mock_validator
args.merge(
form_class: RecaptchaMockForm,
score: params.require(:user)[:recaptcha_mock_score].to_f,
)
elsif FeatureManagement.recaptcha_enterprise?
args.merge(form_class: RecaptchaEnterpriseForm)
else
args
end
end

def redirect_to_signin
controller_info = 'users/sessions#create'
analytics.invalid_authenticity_token(controller: controller_info)
Expand Down
24 changes: 24 additions & 0 deletions app/forms/sign_in_recaptcha_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class SignInRecaptchaForm
RECAPTCHA_ACTION = 'sign_in'

attr_reader :form_class, :form_args

delegate :submit, :errors, to: :form

def initialize(form_class: RecaptchaForm, **form_args)
@form_class = form_class
@form_args = form_args
end

private

def form
@form ||= form_class.new(
score_threshold: IdentityConfig.store.sign_in_recaptcha_score_threshold,
recaptcha_action: RECAPTCHA_ACTION,
**form_args,
)
end
end
7 changes: 6 additions & 1 deletion app/views/devise/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@
},
) %>
<%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %>
<%= f.submit t('links.sign_in'), full_width: true, wide: false %>
<%= render CaptchaSubmitButtonComponent.new(
form: f,
action: SignInRecaptchaForm::RECAPTCHA_ACTION,
button_options: { full_width: true },
).with_content(t('links.sign_in')) %>
<% end %>
<% if desktop_device? %>
<div class='margin-x-neg-1 margin-top-205'>
Expand Down
2 changes: 2 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ session_timeout_in_minutes: 15
session_timeout_warning_seconds: 150
session_total_duration_timeout_in_minutes: 720
ses_configuration_set_name: ''
sign_in_recaptcha_score_threshold: 0.0
sp_handoff_bounce_max_seconds: 2
show_unsupported_passkey_platform_authentication_setup: false
show_user_attribute_deprecation_warnings: false
Expand Down Expand Up @@ -431,6 +432,7 @@ development:
secret_key_base: development_secret_key_base
session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120
show_unsupported_passkey_platform_authentication_setup: true
sign_in_recaptcha_score_threshold: 0.3
skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]'
state_tracking_enabled: true
telephony_adapter: test
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ def self.store
config.add(:show_user_attribute_deprecation_warnings, type: :boolean)
config.add(:short_term_phone_otp_max_attempts, type: :integer)
config.add(:short_term_phone_otp_max_attempt_window_in_seconds, type: :integer)
config.add(:sign_in_recaptcha_score_threshold, type: :float)
config.add(:skip_encryption_allowed_list, type: :json)
config.add(:sp_handoff_bounce_max_seconds, type: :integer)
config.add(:sp_issuer_user_counts_report_configs, type: :json)
Expand Down

0 comments on commit 2dd7c9c

Please sign in to comment.