Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,16 @@ tsconfig.json jsonc

# --------------------------------------------------------------------
# PROJECT-SPEFICIC RULES
# --------------------------------------------------------------------#
# --------------------------------------------------------------------#

## Lock files
knapsack_rspec_report.json lockfile
package-lock.json lockfile
pnpm-lock.yaml lockfile

### Force text diff for Gemfile.lock
### Force text diff for specific lockfiles
Gemfile.lock diff
yarn.lock diff
knapsack_rspec_report.json jsonc

## Executables and runtimes
*.wasm binary
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ lint_gemfile_lock: Gemfile Gemfile.lock ## Lints the Gemfile and its lockfile
lint_yarn_lock: package.json yarn.lock ## Lints the package.json and its lockfile
@yarn install --ignore-scripts
@(! git diff --name-only | grep yarn.lock) || (echo "Error: There are uncommitted changes after running 'yarn install'"; exit 1)
@yarn yarn-deduplicate
@(! git diff --name-only | grep yarn.lock) || (echo "Error: There are duplicate JS dependencies that were removed after running 'yarn yarn-deduplicate'"; exit 1)

lint_lockfiles: lint_gemfile_lock lint_yarn_lock ## Lints to ensure lockfiles are in sync

Expand Down
Binary file added app/assets/images/email/real-id.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/email/state-id-and-passport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions app/assets/stylesheets/email.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ h6 {
@include u-border(1px);
}

.border-top-width-0 {
border-top: 0;
.border-top-0 {
@include u-border-top(0);
}

.border-primary-light {
Expand Down
4 changes: 2 additions & 2 deletions app/components/alert_component.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

class AlertComponent < BaseComponent
VALID_TYPES = %i[info success warning error emergency other].freeze
VALID_TYPES = [nil, :info, :success, :warning, :error, :emergency].freeze

attr_reader :type, :message, :tag_options, :text_tag

def initialize(type: :info, text_tag: 'p', message: nil, **tag_options)
def initialize(type: nil, text_tag: 'p', message: nil, **tag_options)
if !VALID_TYPES.include?(type)
raise ArgumentError, "`type` #{type} is invalid, expected one of #{VALID_TYPES}"
end
Expand Down
3 changes: 2 additions & 1 deletion app/components/captcha_submit_button_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
</div>
<% end %>
<% else %>
<%= f.input(:recaptcha_token, as: :hidden) %>
<%= f.input(:recaptcha_token, as: :hidden, input_html: { value: '' }) %>
<% end %>
<%= render SpinnerButtonComponent.new(
action_message: t('components.captcha_submit_button.action_message'),
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
19 changes: 15 additions & 4 deletions app/controllers/idv/by_mail/request_letter_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ def update_tracking
resend: resend_requested?,
first_letter_requested_at: first_letter_requested_at,
hours_since_first_letter:
gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
phone_step_attempts: gpo_mail_service.phone_step_attempts,
hours_since_first_letter(first_letter_requested_at),
phone_step_attempts: RateLimiter.new(
user: current_user,
rate_limit_type: :proof_address,
).attempts,
**ab_test_analytics_buckets,
)
create_user_event(:gpo_mail_sent, current_user)
Expand All @@ -87,6 +90,11 @@ def first_letter_requested_at
current_user.gpo_verification_pending_profile&.gpo_verification_pending_at
end

def hours_since_first_letter(first_letter_requested_at)
first_letter_requested_at ?
(Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0
end

def confirm_mail_not_rate_limited
redirect_to idv_enter_password_url if gpo_mail_service.rate_limited?
end
Expand All @@ -97,8 +105,11 @@ def resend_letter
resend: true,
first_letter_requested_at: first_letter_requested_at,
hours_since_first_letter:
gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
phone_step_attempts: gpo_mail_service.phone_step_attempts,
hours_since_first_letter(first_letter_requested_at),
phone_step_attempts: RateLimiter.new(
user: current_user,
rate_limit_type: :proof_address,
).attempts,
**ab_test_analytics_buckets,
)
confirmation_maker = confirmation_maker_perform
Expand Down
17 changes: 10 additions & 7 deletions app/controllers/idv/enter_password_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,6 @@ def confirm_current_password
redirect_to idv_enter_password_url
end

def gpo_mail_service
@gpo_mail_service ||= Idv::GpoMail.new(current_user)
end

def init_profile
idv_session.create_profile_from_applicant_with_password(
password,
Expand All @@ -133,10 +129,12 @@ def init_profile
analytics.idv_gpo_address_letter_enqueued(
enqueued_at: Time.zone.now,
resend: false,
phone_step_attempts: gpo_mail_service.phone_step_attempts,
phone_step_attempts: RateLimiter.new(
user: current_user,
rate_limit_type: :proof_address,
).attempts,
first_letter_requested_at: first_letter_requested_at,
hours_since_first_letter:
gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
hours_since_first_letter: hours_since_first_letter(first_letter_requested_at),
**ab_test_analytics_buckets,
)
end
Expand All @@ -155,6 +153,11 @@ def first_letter_requested_at
idv_session.profile.gpo_verification_pending_at
end

def hours_since_first_letter(first_letter_requested_at)
first_letter_requested_at ?
(Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0
end

def valid_password?
current_user.valid_password?(password)
end
Expand Down
39 changes: 33 additions & 6 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 @@ -77,6 +78,36 @@ def process_locked_out_session
redirect_to root_url
end

def valid_captcha_result?
return @valid_captcha_result if defined?(@valid_captcha_result)
@valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit(
email: auth_params[:email],
recaptcha_token: params.require(:user)[:recaptcha_token],
device_cookie: cookies[:device],
).success?
end

def process_failed_captcha
flash[:error] = t('errors.messages.invalid_recaptcha_token')
warden.logout(:user)
warden.lock!
redirect_to root_url
end

def recaptcha_form_args
args = { analytics: }
if IdentityConfig.store.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 Expand Up @@ -125,22 +156,18 @@ def handle_valid_authentication
def track_authentication_attempt(email)
user = User.find_with_email(email) || AnonymousUser.new

success = user_signed_in_and_not_locked_out?(user)
success = current_user.present? && !user_locked_out?(user) && valid_captcha_result?
analytics.email_and_password_auth(
success: success,
user_id: user.uuid,
user_locked_out: user_locked_out?(user),
valid_captcha_result: valid_captcha_result?,
bad_password_count: session[:bad_password_count].to_i,
sp_request_url_present: sp_session[:request_url].present?,
remember_device: remember_device_cookie.present?,
)
end

def user_signed_in_and_not_locked_out?(user)
return false unless current_user
!user_locked_out?(user)
end

def user_locked_out?(user)
user.locked_out?
end
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/users/webauthn_setup_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def confirm
user_session: user_session,
device_name: DeviceName.from_user_agent(request.user_agent),
)
result = form.submit(request.protocol, confirm_params)
result = form.submit(confirm_params)
@platform_authenticator = form.platform_authenticator?
@presenter = WebauthnSetupPresenter.new(
current_user: current_user,
Expand Down Expand Up @@ -161,7 +161,7 @@ def confirm_params
:name,
:platform_authenticator,
:transports,
)
).merge(protocol: request.protocol)
end
end
end
52 changes: 52 additions & 0 deletions app/forms/sign_in_recaptcha_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class SignInRecaptchaForm
include ActiveModel::Model

RECAPTCHA_ACTION = 'sign_in'

attr_reader :form_class, :form_args, :email, :recaptcha_token, :device_cookie

validate :validate_recaptcha_result

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

def submit(email:, recaptcha_token:, device_cookie:)
@email = email
@recaptcha_token = recaptcha_token
@device_cookie = device_cookie

success = valid?
FormResponse.new(success:, errors:, serialize_error_details_only: true)
end

private

def validate_recaptcha_result
recaptcha_response, _assessment_id = recaptcha_form.submit(recaptcha_token)
errors.merge!(recaptcha_form) if !recaptcha_response.success?
end

def device
User.find_with_confirmed_email(email)&.devices&.find_by(cookie_uuid: device_cookie)
end

def score_threshold
if IdentityConfig.store.sign_in_recaptcha_score_threshold.zero? || device.present?
0.0
else
IdentityConfig.store.sign_in_recaptcha_score_threshold
end
end

def recaptcha_form
@recaptcha_form ||= form_class.new(
score_threshold:,
recaptcha_action: RECAPTCHA_ACTION,
**form_args,
)
end
end
Loading