diff --git a/app/controllers/account_reset/pending_controller.rb b/app/controllers/account_reset/pending_controller.rb
index 855a6e12452..d1a713ea934 100644
--- a/app/controllers/account_reset/pending_controller.rb
+++ b/app/controllers/account_reset/pending_controller.rb
@@ -1,7 +1,7 @@
module AccountReset
class PendingController < ApplicationController
include UserAuthenticator
- include ActionView::Helpers::DateHelper
+ include AccountResetConcern
before_action :authenticate_user
before_action :confirm_account_reset_request_exists
@@ -12,7 +12,7 @@ def show
end
def confirm
- @account_reset_deletion_period_interval = account_reset_deletion_period_interval
+ @account_reset_deletion_period_interval = account_reset_deletion_period_interval(current_user)
end
def cancel
@@ -32,16 +32,5 @@ def pending_account_reset_request
current_user,
).call
end
-
- def account_reset_deletion_period_interval
- current_time = Time.zone.now
-
- distance_of_time_in_words(
- current_time,
- current_time + IdentityConfig.store.account_reset_wait_period_days.days,
- true,
- accumulate_on: :hours,
- )
- end
end
end
diff --git a/app/controllers/account_reset/request_controller.rb b/app/controllers/account_reset/request_controller.rb
index afc55e4df98..7f51aea7740 100644
--- a/app/controllers/account_reset/request_controller.rb
+++ b/app/controllers/account_reset/request_controller.rb
@@ -1,13 +1,13 @@
module AccountReset
class RequestController < ApplicationController
include TwoFactorAuthenticatable
- include ActionView::Helpers::DateHelper
+ include AccountResetConcern
before_action :confirm_two_factor_enabled
def show
analytics.account_reset_visit
- @account_reset_deletion_period_interval = account_reset_deletion_period_interval
+ @account_reset_deletion_period_interval = account_reset_deletion_period_interval(current_user)
end
def create
@@ -41,16 +41,5 @@ def analytics_attributes
email_addresses: current_user.email_addresses.count,
}
end
-
- def account_reset_deletion_period_interval
- current_time = Time.zone.now
-
- distance_of_time_in_words(
- current_time,
- current_time + IdentityConfig.store.account_reset_wait_period_days.days,
- true,
- accumulate_on: :hours,
- )
- end
end
end
diff --git a/app/controllers/concerns/account_reset_concern.rb b/app/controllers/concerns/account_reset_concern.rb
new file mode 100644
index 00000000000..15457c8fb87
--- /dev/null
+++ b/app/controllers/concerns/account_reset_concern.rb
@@ -0,0 +1,38 @@
+module AccountResetConcern
+ include ActionView::Helpers::DateHelper
+ def account_reset_deletion_period_interval(user)
+ current_time = Time.zone.now
+
+ distance_of_time_in_words(
+ current_time,
+ current_time + account_reset_wait_period_days(user),
+ true,
+ accumulate_on: reset_accumulation_type(user),
+ )
+ end
+
+ def account_reset_wait_period_days(user)
+ if supports_fraud_account_reset?(user)
+ IdentityConfig.store.account_reset_fraud_user_wait_period_days.days
+ else
+ IdentityConfig.store.account_reset_wait_period_days.days
+ end
+ end
+
+ def supports_fraud_account_reset?(user)
+ IdentityConfig.store.account_reset_fraud_user_wait_period_days.present? &&
+ fraud_state?(user)
+ end
+
+ def fraud_state?(user)
+ user.fraud_review_pending? || user.fraud_rejection?
+ end
+
+ def reset_accumulation_type(user)
+ if account_reset_wait_period_days(user) > 3.days
+ :days
+ else
+ :hours
+ end
+ end
+end
diff --git a/app/controllers/concerns/fraud_review_concern.rb b/app/controllers/concerns/fraud_review_concern.rb
index c70dce1b2fc..585efd4bc7f 100644
--- a/app/controllers/concerns/fraud_review_concern.rb
+++ b/app/controllers/concerns/fraud_review_concern.rb
@@ -26,8 +26,7 @@ def handle_fraud_rejection
def in_person_prevent_fraud_redirection?
IdentityConfig.store.in_person_proofing_enforce_tmx &&
- !current_user.in_person_enrollment_status.nil? &&
- current_user.in_person_enrollment_status != 'passed'
+ current_user.ipp_enrollment_status_not_passed?
end
def redirect_to_fraud_review
diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb
index 0f082a8b0c1..cb46d134e7b 100644
--- a/app/controllers/concerns/rate_limit_concern.rb
+++ b/app/controllers/concerns/rate_limit_concern.rb
@@ -43,14 +43,7 @@ def rate_limit_redirect!(rate_limit_type)
def track_rate_limited_event(rate_limit_type)
analytics_args = { limiter_type: rate_limit_type }
- limiter_context = 'single-session'
-
- if rate_limit_type == :proof_address
- analytics_args[:step_name] = :phone
- elsif rate_limit_type == :proof_ssn
- analytics_args[:step_name] = 'verify_info'
- limiter_context = 'multi-session'
- end
+ limiter_context = rate_limit_type == :proof_ssn ? 'multi-session' : 'single-session'
irs_attempts_api_tracker.idv_verification_rate_limited(limiter_context: limiter_context)
analytics.rate_limit_reached(**analytics_args)
diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb
index 3e4a139b7a1..a4373d9ee05 100644
--- a/app/controllers/concerns/saml_idp_auth_concern.rb
+++ b/app/controllers/concerns/saml_idp_auth_concern.rb
@@ -55,9 +55,7 @@ def check_sp_active
def validate_service_provider_and_authn_context
return if result.success?
- analytics.saml_auth(
- **result.to_h.merge(request_signed: saml_request.signed?),
- )
+ capture_analytics
render 'saml_idp/auth/error', status: :bad_request
end
diff --git a/app/controllers/concerns/verify_profile_concern.rb b/app/controllers/concerns/verify_profile_concern.rb
index 5d29d7178d6..33d39c7de27 100644
--- a/app/controllers/concerns/verify_profile_concern.rb
+++ b/app/controllers/concerns/verify_profile_concern.rb
@@ -4,6 +4,9 @@ module VerifyProfileConcern
def url_for_pending_profile_reason
return idv_verify_by_mail_enter_code_url if current_user.gpo_verification_pending_profile?
return idv_in_person_ready_to_verify_url if current_user.in_person_pending_profile?
+ # We don't want to hit idv_please_call_url in cases where the user
+ # has fraud review pending and not passed at the post office
+ return idv_welcome_url if user_failed_ipp_with_fraud_review_pending?
return idv_please_call_url if current_user.fraud_review_pending?
idv_not_verified_url if current_user.fraud_rejection?
end
@@ -19,4 +22,10 @@ def pending_profile_policy
biometric_comparison_requested: nil,
)
end
+
+ def user_failed_ipp_with_fraud_review_pending?
+ IdentityConfig.store.in_person_proofing_enforce_tmx &&
+ current_user.ipp_enrollment_status_not_passed? &&
+ current_user.fraud_review_pending?
+ end
end
diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb
index 6dbb94bd0d5..b8e58373ba5 100644
--- a/app/controllers/idv/by_mail/enter_code_controller.rb
+++ b/app/controllers/idv/by_mail/enter_code_controller.rb
@@ -11,34 +11,21 @@ class EnterCodeController < ApplicationController
before_action :confirm_verification_needed
def index
- # GPO reminder emails include an "I did not receive my letter!" link that results in
- # slightly different copy on this screen.
- @user_did_not_receive_letter = !!params[:did_not_receive_letter]
-
analytics.idv_verify_by_mail_enter_code_visited(
- source: if @user_did_not_receive_letter then 'gpo_reminder_email' end,
+ source: user_did_not_receive_letter? ? 'gpo_reminder_email' : nil,
+ otp_rate_limited: rate_limiter.limited?,
+ user_can_request_another_letter: user_can_request_another_letter?,
)
if rate_limiter.limited?
- redirect_to idv_enter_code_rate_limited_url
- return
+ return redirect_to idv_enter_code_rate_limited_url
+ elsif pii_locked?
+ return redirect_to capture_password_url
end
- @last_date_letter_was_sent = last_date_letter_was_sent
- @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii)
- @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code?
-
- gpo_mail = Idv::GpoMail.new(current_user)
- @can_request_another_letter =
- FeatureManagement.gpo_verification_enabled? &&
- !gpo_mail.rate_limited? &&
- !gpo_mail.profile_too_old?
-
- if pii_locked?
- redirect_to capture_password_url
- else
- render :index
- end
+ prefilled_code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code?
+ @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii, otp: prefilled_code)
+ render_enter_code_form
end
def pii
@@ -66,19 +53,23 @@ def create
if rate_limiter.limited?
redirect_to idv_enter_code_rate_limited_url
else
- flash[:error] = @gpo_verify_form.errors.first.message if !rate_limiter.limited?
- redirect_to idv_verify_by_mail_enter_code_url
+ render_enter_code_form
end
- return
+ else
+ prepare_for_personal_key
+ redirect_to idv_personal_key_url
end
-
- prepare_for_personal_key
-
- redirect_to idv_personal_key_url
end
private
+ def render_enter_code_form
+ @can_request_another_letter = user_can_request_another_letter?
+ @user_did_not_receive_letter = user_did_not_receive_letter?
+ @last_date_letter_was_sent = last_date_letter_was_sent
+ render :index
+ end
+
def pending_in_person_enrollment?
return false unless IdentityConfig.store.in_person_proofing_enabled
current_user.pending_in_person_enrollment.present?
@@ -150,9 +141,29 @@ def pii_locked?
!Pii::Cacher.new(current_user, user_session).exists_in_session?
end
+ # GPO reminder emails include an "I did not receive my letter!" link that results in
+ # slightly different copy on this screen.
+ def user_did_not_receive_letter?
+ !!params[:did_not_receive_letter]
+ end
+
+ def user_can_request_another_letter?
+ return @user_can_request_another_letter if defined?(@user_can_request_another_letter)
+ gpo_mail = Idv::GpoMail.new(current_user)
+ @user_can_request_another_letter =
+ FeatureManagement.gpo_verification_enabled? &&
+ !gpo_mail.rate_limited? &&
+ !gpo_mail.profile_too_old?
+ end
+
def last_date_letter_was_sent
- current_user.gpo_verification_pending_profile&.gpo_confirmation_codes&.
- pluck(:updated_at)&.max
+ return @last_date_letter_was_sent if defined?(@last_date_letter_was_sent)
+
+ @last_date_letter_was_sent = current_user.
+ gpo_verification_pending_profile&.
+ gpo_confirmation_codes&.
+ pluck(:updated_at)&.
+ max
end
end
end
diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb
index 99ff49e8da4..d266ba6297b 100644
--- a/app/controllers/idv/document_capture_controller.rb
+++ b/app/controllers/idv/document_capture_controller.rb
@@ -7,7 +7,7 @@ class DocumentCaptureController < ApplicationController
include StepIndicatorConcern
before_action :confirm_not_rate_limited, except: [:update]
- before_action :confirm_step_allowed
+ before_action :confirm_step_allowed, unless: -> { allow_direct_ipp? }
before_action :override_csp_to_allow_acuant
def show
@@ -47,6 +47,7 @@ def extra_view_variables
sp_name: decorated_sp_session.sp_name,
failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'),
skip_doc_auth: idv_session.skip_doc_auth,
+ skip_doc_auth_from_handoff: idv_session.skip_doc_auth_from_handoff,
opted_in_to_in_person_proofing: idv_session.opted_in_to_in_person_proofing,
doc_auth_selfie_capture: decorated_sp_session.selfie_required?,
}.merge(
@@ -62,6 +63,7 @@ def self.step_info
preconditions: ->(idv_session:, user:) {
idv_session.flow_path == 'standard' && (
# mobile
+ idv_session.skip_doc_auth_from_handoff ||
idv_session.skip_hybrid_handoff ||
idv_session.skip_doc_auth ||
!idv_session.selfie_check_required || # desktop but selfie not required
@@ -109,5 +111,22 @@ def handle_stored_result
failure(I18n.t('doc_auth.errors.general.network_error'), extra)
end
end
+
+ def allow_direct_ipp?
+ return false unless idv_session.welcome_visited &&
+ idv_session.idv_consent_given
+ # not allowed when no step param and action:show(get request)
+ return false if params[:step].blank? || params[:action].to_s != 'show' ||
+ idv_session.flow_path == 'hybrid'
+ # Only allow direct access to document capture if IPP available
+ return false unless IdentityConfig.store.in_person_doc_auth_button_enabled &&
+ Idv::InPersonConfig.enabled_for_issuer?(decorated_sp_session.sp_issuer)
+ @previous_step_url = params[:step] == 'hybrid_handoff' ? idv_hybrid_handoff_path : nil
+ # allow
+ idv_session.flow_path = 'standard'
+ idv_session.skip_doc_auth_from_handoff = true
+ idv_session.skip_hybrid_handoff = nil
+ true
+ end
end
end
diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb
index 829ff2abe7c..0625337bd78 100644
--- a/app/controllers/idv/hybrid_handoff_controller.rb
+++ b/app/controllers/idv/hybrid_handoff_controller.rb
@@ -13,6 +13,11 @@ def show
@upload_disabled = idv_session.selfie_check_required &&
!idv_session.desktop_selfie_test_mode_enabled?
+ @direct_ipp_with_selfie_enabled = IdentityConfig.store.in_person_doc_auth_button_enabled &&
+ Idv::InPersonConfig.enabled_for_issuer?(
+ decorated_sp_session.sp_issuer,
+ )
+
@selfie_required = idv_session.selfie_check_required
analytics.idv_doc_auth_hybrid_handoff_visited(**analytics_arguments)
@@ -22,6 +27,8 @@ def show
true
)
+ # reset if we visit or come back
+ idv_session.skip_doc_auth_from_handoff = nil
render :show, locals: extra_view_variables
end
@@ -55,7 +62,9 @@ def self.step_info
next_steps: [:link_sent, :document_capture],
preconditions: ->(idv_session:, user:) {
idv_session.idv_consent_given &&
- self.selected_remote(idv_session: idv_session)
+ (self.selected_remote(idv_session: idv_session) || # from opt-in screen
+ # back from ipp doc capture screen
+ idv_session.skip_doc_auth_from_handoff)
},
undo_step: ->(idv_session:, user:) do
idv_session.flow_path = nil
diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb
index c99e007ac44..1ae381f7868 100644
--- a/app/controllers/idv/link_sent_controller.rb
+++ b/app/controllers/idv/link_sent_controller.rb
@@ -72,7 +72,7 @@ def handle_document_verification_success
def render_document_capture_cancelled
redirect_to idv_hybrid_handoff_url
idv_session.flow_path = nil
- failure(I18n.t('errors.doc_auth.document_capture_cancelled'))
+ failure(I18n.t('errors.doc_auth.document_capture_canceled'))
end
def render_step_incomplete_error
diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb
index d057729a6ad..9c3b3483c15 100644
--- a/app/controllers/openid_connect/authorization_controller.rb
+++ b/app/controllers/openid_connect/authorization_controller.rb
@@ -156,6 +156,7 @@ def pre_validate_authorize_form
**result.to_h.except(:redirect_uri, :code_digest).merge(
user_fully_authenticated: user_fully_authenticated?,
referer: request.referer,
+ vtr_param: params[:vtr],
),
)
return if result.success?
@@ -214,6 +215,8 @@ def track_events
ial: event_ial_context.ial,
billed_ial: event_ial_context.bill_for_ial_1_or_2,
sign_in_flow: session[:sign_in_flow],
+ vtr: sp_session[:vtr],
+ acr_values: sp_session[:acr_values],
)
track_billing_events
end
diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb
index e2d1ac49c3d..a09148c54af 100644
--- a/app/controllers/saml_idp_controller.rb
+++ b/app/controllers/saml_idp_controller.rb
@@ -136,6 +136,7 @@ def log_external_saml_auth_request
analytics.saml_auth_request(
requested_ial: requested_ial,
+ authn_context: saml_request&.requested_authn_contexts,
requested_aal_authn_context: saml_request&.requested_aal_authn_context,
requested_vtr_authn_context: saml_request&.requested_vtr_authn_context,
force_authn: saml_request&.force_authn?,
@@ -181,6 +182,8 @@ def track_events
ial: resolved_authn_context_int_ial,
billed_ial: ial_context.bill_for_ial_1_or_2,
sign_in_flow: session[:sign_in_flow],
+ vtr: sp_session[:vtr],
+ acr_values: sp_session[:acr_values],
)
track_billing_events
end
diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx
index 932cc3342db..86dab1889a2 100644
--- a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx
+++ b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx
@@ -24,12 +24,13 @@ function AcuantSelfieCaptureCanvas({ imageCaptureText, onSelfieCaptureClosed })
return (
<>
{!isReady &&
- {imageCaptureText && ( - {imageCaptureText} - )} -
++ {imageCaptureText && ( + {imageCaptureText} + )} +
+<%= t( 'account_reset.pending.wait_html', - hours: @pending_presenter.account_reset_deletion_period_hours, + waiting_period: @pending_presenter.account_reset_deletion_period, interval: @pending_presenter.time_remaining_until_granted, ) %>
diff --git a/app/views/idv/by_mail/enter_code/_did_not_receive_letter.html.erb b/app/views/idv/by_mail/enter_code/_did_not_receive_letter.html.erb new file mode 100644 index 00000000000..d6b970d9eca --- /dev/null +++ b/app/views/idv/by_mail/enter_code/_did_not_receive_letter.html.erb @@ -0,0 +1,52 @@ +<% if !@can_request_another_letter %> + <%= render AlertComponent.new(type: :warning, class: 'margin-bottom-4') do %> + <%= t( + 'idv.gpo.alert_rate_limit_warning_html', + date_letter_was_sent: I18n.l( + @last_date_letter_was_sent, + format: :event_date, + ), + ) %> + <% end %> +<% end %> + +<%= render AlertComponent.new(type: :info, class: 'margin-bottom-4', text_tag: 'div') do %> +
+ <%= t('idv.gpo.alert_info') %>
+
+ <%= render 'shared/address', address: @gpo_verify_form.pii %>
+
+ <%= t('idv.gpo.wrong_address') %> + <%= link_to t('idv.gpo.clear_and_start_over'), idv_confirm_start_over_path %> +
+<% end %> + +<%= render PageHeadingComponent.new.with_content(t('idv.gpo.did_not_receive_letter.title')) %> + +<% if @can_request_another_letter %> + <%= t( + 'idv.gpo.did_not_receive_letter.intro.request_new_letter_prompt_html', + request_new_letter_link: link_to( + t('idv.gpo.did_not_receive_letter.intro.request_new_letter_link'), + idv_request_letter_path, + ), + ) %> +<% end %> +<%= t('idv.gpo.did_not_receive_letter.intro.be_patient_html') %> + ++ <%= t('idv.gpo.did_not_receive_letter.form.instructions') %> +
+ +<%= render 'form' %> + +<%= link_to t('idv.gpo.return_to_profile'), account_path %> + +
+ <%= t('idv.gpo.alert_info') %>
+
+ <%= render 'shared/address', address: @gpo_verify_form.pii %>
+
+ <%= t('idv.gpo.wrong_address') %> + <%= link_to t('idv.gpo.clear_and_start_over'), idv_confirm_start_over_path %> +
+<% end %> + +<%= render PageHeadingComponent.new.with_content(t('idv.gpo.title')) %> + +<%= t('idv.gpo.intro_html') %> + ++ <%= t('idv.gpo.form.instructions') %> +
+ +<%= render 'form' %> + +<% if @can_request_another_letter %> + <%= link_to t('idv.messages.gpo.resend'), idv_request_letter_path, class: 'display-block margin-bottom-2' %> +<% end %> + +<%= link_to t('idv.gpo.return_to_profile'), account_path %> + +
- <%= t('idv.gpo.alert_info') %>
-
- <%= render 'shared/address', address: @gpo_verify_form.pii %>
-
- <%= t('idv.gpo.wrong_address') %> - <%= link_to t('idv.gpo.clear_and_start_over'), idv_confirm_start_over_path %> -
-<% end %> - -<%= render PageHeadingComponent.new.with_content( - if @user_did_not_receive_letter - t('idv.gpo.did_not_receive_letter.title') - else - t('idv.gpo.title') - end, - ) %> - -<% if @user_did_not_receive_letter %> - <% if @can_request_another_letter %> - <%= t( - 'idv.gpo.did_not_receive_letter.intro.request_new_letter_prompt_html', - request_new_letter_link: link_to( - t('idv.gpo.did_not_receive_letter.intro.request_new_letter_link'), - idv_request_letter_path, - ), - ) %> - <% end %> - <%= t('idv.gpo.did_not_receive_letter.intro.be_patient_html') %> -<% else %> - <%= t('idv.gpo.intro_html') %> -<% end %> -- <%= if @user_did_not_receive_letter - t('idv.gpo.did_not_receive_letter.form.instructions') - else - t('idv.gpo.form.instructions') - end %> -
- -<%= simple_form_for( - @gpo_verify_form, - url: idv_verify_by_mail_enter_code_path, - html: { autocomplete: 'off', method: :post }, - ) do |f| %> -<%= t('doc_auth.info.how_to_verify') %>
-<%= simple_form_for( - @idv_how_to_verify_form, - html: { autocomplete: 'off' }, - method: :put, - url: idv_how_to_verify_url, - ) do |f| -%> -<%= t('doc_auth.info.verify_online_instruction') %>
-<%= t('doc_auth.info.verify_online_description') %>
- -<%= t('doc_auth.info.verify_online_instruction') %>
+<%= t('doc_auth.info.verify_online_description') %>
+<%= t('doc_auth.info.verify_at_post_office_instruction') %>
<%= t('doc_auth.info.verify_at_post_office_description') %>
- -+ <%= t('doc_auth.info.hybrid_handoff_ipp_html') %> +
++ <%= link_to t('in_person_proofing.headings.prepare'), idv_document_capture_path(step: :hybrid_handoff) %> +
+- <%= t('user_mailer.account_reset_granted.intro_html', hours: @account_reset_deletion_period_hours, app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray')) %> + <%= t('user_mailer.account_reset_granted.intro_html', waiting_period: @account_reset_deletion_period_interval, app_name: link_to(APP_NAME, IdentityConfig.store.mailer_domain_name, class: 'gray')) %>