diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index c21d2bcec7e..916ee1b6a7c 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -15,6 +15,7 @@ @forward 'icon'; @forward 'language-picker'; @forward 'list'; +@forward 'modal'; @forward 'nav'; @forward 'page-heading'; @forward 'password'; diff --git a/app/components/modal_component.scss b/app/assets/stylesheets/components/_modal.scss similarity index 95% rename from app/components/modal_component.scss rename to app/assets/stylesheets/components/_modal.scss index c92cf63bcc9..7611bc77fc0 100644 --- a/app/components/modal_component.scss +++ b/app/assets/stylesheets/components/_modal.scss @@ -1,5 +1,5 @@ @use 'uswds-core' as *; -@use 'variables/app' as *; +@use '../variables/app' as *; .usa-modal-overlay { // Temporary styles to avoid inheriting too much of the USWDS opinionated modal styling until diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 0fad0e3b67f..0ddfcad2024 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -29,9 +29,8 @@ def pii_from_doc flow_session['pii_from_doc'] end - # copied from doc_auth_controller def flow_path - idv_session.flow_path || flow_session[:flow_path] + idv_session.flow_path end private diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index 8001c42632e..dda0c8b750f 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -49,7 +49,6 @@ def analytics_arguments def skip_to_capture flow_session[:skip_upload_step] = true idv_session.flow_path = 'standard' - flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy end def consent_form_params diff --git a/app/controllers/idv/doc_auth_controller.rb b/app/controllers/idv/doc_auth_controller.rb deleted file mode 100644 index 8c8573527b9..00000000000 --- a/app/controllers/idv/doc_auth_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Idv - class DocAuthController < ApplicationController - def index - log_unexpected_visit('DocAuthController index') - - redirect_to idv_welcome_url - end - - def show - log_unexpected_visit('DocAuthController show') - - redirect_to idv_welcome_url - end - - def update - log_unexpected_visit('DocAuthController update') - - redirect_to idv_welcome_url - end - - def return_to_sp - log_unexpected_visit('DocAuthController return_to_sp', location: params[:location]) - redirect_to return_to_sp_failure_to_proof_url( - step: params[:step], - location: params[:location], - ) - end - - # Temporary logging to see if we're using these routes anywhere - def log_unexpected_visit(from, **extra) - extra[:referer] = request.referer - extra[:step] = params[:step] - analytics.track_event(from, **extra) - end - end -end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index cacfa95cdec..ba4a87a6ffd 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -52,7 +52,6 @@ def extra_view_variables def confirm_hybrid_handoff_complete return if idv_session.flow_path.present? - return if flow_session[:flow_path].present? # remove in future deploy redirect_to idv_hybrid_handoff_url end diff --git a/app/controllers/idv/gpo_controller.rb b/app/controllers/idv/gpo_controller.rb index e361f53840b..0ba648025ef 100644 --- a/app/controllers/idv/gpo_controller.rb +++ b/app/controllers/idv/gpo_controller.rb @@ -27,6 +27,7 @@ def create redirect_to capture_password_url elsif resend_requested? resend_letter + flash[:success] = t('idv.messages.gpo.another_letter_on_the_way') redirect_to idv_come_back_later_url else redirect_to idv_review_url diff --git a/app/controllers/idv/gpo_verify_controller.rb b/app/controllers/idv/gpo_verify_controller.rb index db9beadc674..87b18a64e2a 100644 --- a/app/controllers/idv/gpo_verify_controller.rb +++ b/app/controllers/idv/gpo_verify_controller.rb @@ -9,6 +9,12 @@ class GpoVerifyController < ApplicationController def index analytics.idv_gpo_verification_visited + + if rate_limiter.limited? + render_rate_limited + return + end + gpo_mail = Idv::GpoMail.new(current_user) @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii) @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code? @@ -18,9 +24,7 @@ def index !gpo_mail.mail_spammed? && !gpo_mail.profile_too_old? - if rate_limiter.limited? - render_rate_limited - elsif pii_locked? + if pii_locked? redirect_to capture_password_url else render :index @@ -32,13 +36,13 @@ def pii end def create - @gpo_verify_form = build_gpo_verify_form - - rate_limiter.increment! if rate_limiter.limited? render_rate_limited return end + rate_limiter.increment! + + @gpo_verify_form = build_gpo_verify_form result = @gpo_verify_form.submit analytics.idv_gpo_verification_submitted(**result.to_h) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 2539f737034..12a400b82c9 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -38,11 +38,10 @@ def hybrid_flow_chosen? end def handle_phone_submission - rate_limiter.increment! return rate_limited_failure if rate_limiter.limited? + rate_limiter.increment! idv_session.phone_for_mobile_flow = params[:doc_auth][:phone] idv_session.flow_path = 'hybrid' - flow_session[:flow_path] = 'hybrid' # temp addition for 50/50 remove in future deploy telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) @@ -62,7 +61,6 @@ def handle_phone_submission else redirect_to idv_hybrid_handoff_url idv_session.flow_path = nil - flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy end analytics.idv_doc_auth_upload_submitted( @@ -99,7 +97,7 @@ def build_telephony_form_response(telephony_result) extra: { telephony_response: telephony_result.to_h, destination: :link_sent, - flow_path: idv_session.flow_path || flow_session[:flow_path], # remove in future deploy + flow_path: idv_session.flow_path, }, ) end @@ -116,7 +114,6 @@ def update_document_capture_session_requested_at(session_uuid) def bypass_send_link_steps idv_session.flow_path = 'standard' - flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy redirect_to idv_document_capture_url analytics.idv_doc_auth_upload_submitted( @@ -214,14 +211,11 @@ def confirm_hybrid_handoff_needed setup_for_redo if params[:redo] idv_session.flow_path = 'standard' if flow_session[:skip_upload_step] - # next line temp added for 50/50, remove in future deploy - flow_session[:flow_path] = 'standard' if flow_session[:skip_upload_step] - # flow_session temp added for 50/50, remove in future deploy. - return if !idv_session.flow_path && !flow_session[:flow_path] + return if !idv_session.flow_path - if idv_session.flow_path == 'standard' || flow_session[:flow_path] == 'standard' + if idv_session.flow_path == 'standard' redirect_to idv_document_capture_url - elsif idv_session.flow_path == 'hybrid' || flow_session[:flow_path] == 'hybrid' + elsif idv_session.flow_path == 'hybrid' redirect_to idv_link_sent_url end end @@ -230,10 +224,8 @@ def setup_for_redo flow_session[:redo_document_capture] = true if flow_session[:skip_upload_step] idv_session.flow_path = 'standard' - flow_session[:flow_path] = 'standard' # temp added for 50/50, remove in future deploy else idv_session.flow_path = nil - flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy end end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 9db8ca8f2eb..bc2c9acf34a 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -40,9 +40,8 @@ def extra_view_variables def confirm_hybrid_handoff_complete return if idv_session.flow_path == 'hybrid' - return if flow_session[:flow_path] == 'hybrid' - if idv_session.flow_path == 'standard' || flow_session[:flow_path] == 'standard' + if idv_session.flow_path == 'standard' redirect_to idv_document_capture_url else redirect_to idv_hybrid_handoff_url @@ -71,13 +70,11 @@ def handle_document_verification_success(get_results_response) save_proofing_components(current_user) extract_pii_from_doc(current_user, get_results_response, store_in_session: true) idv_session.flow_path = 'hybrid' - flow_session[:flow_path] = 'hybrid' # temp added for 50/50, remove in future deploy end def render_document_capture_cancelled redirect_to idv_hybrid_handoff_url idv_session.flow_path = nil - flow_session[:flow_path] = nil # temp added for 50/50, remove in future deploy failure(I18n.t('errors.doc_auth.document_capture_cancelled')) end diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb index 24d07fef588..6a1437a45a0 100644 --- a/app/controllers/idv/review_controller.rb +++ b/app/controllers/idv/review_controller.rb @@ -52,7 +52,13 @@ def create user_session[:need_personal_key_confirmation] = true - flash[:success] = t('idv.messages.confirm') + flash[:success] = + if gpo_user_flow? + t('idv.messages.gpo.letter_on_the_way') + else + t('idv.messages.confirm') + end + redirect_to next_step analytics.idv_review_complete( diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index 968737e7c90..02c0bf9b16a 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -29,7 +29,6 @@ def update if flow_session['redo_document_capture'] flow_session.delete('redo_document_capture') idv_session.flow_path ||= 'standard' - flow_session[:flow_path] ||= 'standard' # temp added for 50/50, remove in future deploy end redirect_to idv_verify_info_url diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index 2e56b55708f..9804a65166f 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -51,7 +51,7 @@ def handle_valid_confirmation_otp end def otp_verification_form - OtpVerificationForm.new(current_user, sanitized_otp_code) + OtpVerificationForm.new(current_user, sanitized_otp_code, phone_configuration) end def redirect_if_blank_phone diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index 125effcc62a..d3dba149012 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -14,15 +14,23 @@ def show def create @personal_key_form = PersonalKeyForm.new(current_user, personal_key_param) result = @personal_key_form.submit - analytics_hash = result.to_h.merge(multi_factor_auth_method: 'personal-key') - - analytics.track_mfa_submit_event(analytics_hash) + track_analytics(result) handle_result(result) end private + def track_analytics(result) + mfa_created_at = current_user.encrypted_recovery_code_digest_generated_at + analytics_hash = result.to_h.merge( + multi_factor_auth_method: 'personal-key', + multi_factor_auth_method_created_at: mfa_created_at, + ) + + analytics.track_mfa_submit_event(analytics_hash) + end + def check_personal_key_enabled return if TwoFactorAuthentication::PersonalKeyPolicy.new(current_user).enabled? diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 01d87c985d6..ec3503d4a81 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -56,18 +56,14 @@ def handle_valid_webauthn def handle_invalid_webauthn is_platform_auth = params[:platform].to_s == 'true' if is_platform_auth - if presenter_for_two_factor_authentication_method.multiple_factors_enabled? - flash[:error] = t( - 'two_factor_authentication.webauthn_error.multiple_methods', - link: view_context.link_to( - t('two_factor_authentication.webauthn_error.additional_methods_link'), - login_two_factor_options_path, - ), - ) - redirect_to login_two_factor_webauthn_url(platform: params[:platform]) - else - redirect_to login_two_factor_webauthn_error_url - end + flash[:error] = t( + 'two_factor_authentication.webauthn_error.multiple_methods', + link: view_context.link_to( + t('two_factor_authentication.webauthn_error.additional_methods_link'), + login_two_factor_options_path, + ), + ) + redirect_to login_two_factor_webauthn_url(platform: 'true') else flash[:error] = t('errors.general') redirect_to login_two_factor_webauthn_url @@ -112,6 +108,7 @@ def analytics_properties context: context, multi_factor_auth_method: auth_method, webauthn_configuration_id: form&.webauthn_configuration&.id, + multi_factor_auth_method_created_at: form&.webauthn_configuration&.created_at, } end diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index db00adbc0e4..44e9b88c838 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -17,12 +17,18 @@ def submit(params) attr_reader :user, :backup_code def valid_backup_code? - BackupCodeGenerator.new(@user).verify(backup_code) + backup_code_config.present? + end + + def backup_code_config + @backup_code_config ||= BackupCodeGenerator.new(@user). + if_valid_consume_code_return_config(backup_code) end def extra_analytics_attributes { multi_factor_auth_method: 'backup_code', + multi_factor_auth_method_created_at: backup_code_config.created_at, } end end diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index 2a26f78d205..8f8c919592c 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -260,7 +260,8 @@ def scopes def validate_privileges if (ial2_requested? && !ial_context.ial2_service_provider?) || - (ial_context.ialmax_requested? && !ial_context.ial2_service_provider?) + (ial_context.ialmax_requested? && + !IdentityConfig.store.allowed_ialmax_providers.include?(client_id)) errors.add( :acr_values, t('openid_connect.authorization.errors.no_auth'), type: :no_auth diff --git a/app/forms/otp_verification_form.rb b/app/forms/otp_verification_form.rb index fa81af7c01e..2434d8c5434 100644 --- a/app/forms/otp_verification_form.rb +++ b/app/forms/otp_verification_form.rb @@ -8,9 +8,10 @@ class OtpVerificationForm validate :validate_user_otp_expiration validate :validate_code_equals_user_otp - def initialize(user, code) + def initialize(user, code, phone_configuration) @user = user @code = code + @phone_configuration = phone_configuration end def submit @@ -28,7 +29,7 @@ def submit private - attr_reader :code, :user + attr_reader :code, :user, :phone_configuration def validate_code_length return if code.blank? || code.size == TwoFactorAuthenticatable::DIRECT_OTP_LENGTH @@ -63,8 +64,11 @@ def otp_expired? end def extra_analytics_attributes + multi_factor_auth_method_created_at = phone_configuration&.created_at + { multi_factor_auth_method: 'otp_code', + multi_factor_auth_method_created_at: multi_factor_auth_method_created_at, } end end diff --git a/app/forms/totp_verification_form.rb b/app/forms/totp_verification_form.rb index f8485dc1bd7..930faba72fe 100644 --- a/app/forms/totp_verification_form.rb +++ b/app/forms/totp_verification_form.rb @@ -8,7 +8,7 @@ def submit cfg = if_valid_totp_code_return_config FormResponse.new( success: cfg.present?, - extra: extra_analytics_attributes(cfg&.id), + extra: extra_analytics_attributes(cfg), ) end @@ -29,10 +29,11 @@ def totp_code_length TwoFactorAuthenticatable::OTP_LENGTH end - def extra_analytics_attributes(cfg_id) + def extra_analytics_attributes(cfg) { multi_factor_auth_method: 'totp', - auth_app_configuration_id: cfg_id, + auth_app_configuration_id: cfg&.id, + multi_factor_auth_method_created_at: cfg&.created_at, } end end diff --git a/app/forms/user_piv_cac_verification_form.rb b/app/forms/user_piv_cac_verification_form.rb index 25f753f8494..5789f357aae 100644 --- a/app/forms/user_piv_cac_verification_form.rb +++ b/app/forms/user_piv_cac_verification_form.rb @@ -3,7 +3,7 @@ class UserPivCacVerificationForm include PivCacFormHelpers attr_accessor :x509_dn_uuid, :x509_dn, :x509_issuer, :token, :error_type, :nonce, :user, :key_id, - :piv_cac_required, :piv_cac_configuration + :piv_cac_required validates :token, presence: true validates :nonce, presence: true @@ -20,6 +20,10 @@ def submit ) end + def piv_cac_configuration + @piv_cac_configuration ||= ::PivCacConfiguration.find_by(x509_dn_uuid: x509_dn_uuid) + end + private def valid_submission? @@ -29,7 +33,6 @@ def valid_submission? end def x509_cert_matches - piv_cac_configuration = ::PivCacConfiguration.find_by(x509_dn_uuid: x509_dn_uuid) if user == piv_cac_configuration&.user true else @@ -51,6 +54,7 @@ def extra_analytics_attributes { multi_factor_auth_method: 'piv_cac', piv_cac_configuration_id: piv_cac_configuration&.id, + multi_factor_auth_method_created_at: piv_cac_configuration&.created_at, } end end diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 287f2fe85bb..1e4078cc2be 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -566,7 +566,7 @@ function AcuantCapture( {isMobile && hasCapture && allowUpload && - formatHTML(t('doc_auth.buttons.take_or_upload_picture'), { + formatHTML(t('doc_auth.buttons.take_or_upload_picture_html'), { 'lg-take-photo': () => null, 'lg-upload': ({ children }) => ( diff --git a/app/javascript/packages/document-capture/components/hybrid-doc-capture-warning.tsx b/app/javascript/packages/document-capture/components/hybrid-doc-capture-warning.tsx index 72f941d229e..f944aa59e11 100644 --- a/app/javascript/packages/document-capture/components/hybrid-doc-capture-warning.tsx +++ b/app/javascript/packages/document-capture/components/hybrid-doc-capture-warning.tsx @@ -28,10 +28,10 @@ function HybridDocCaptureWarning({ className = '' }: HybridDocCaptureWarningProp const appName = getConfigValue('appName'); const listHeadingText = t('doc_auth.hybrid_flow_warning.only_add_if_text'); - const ownAccountItemText = t('doc_auth.hybrid_flow_warning.only_add_own_account_html', { + const ownAccountItemText = t('doc_auth.hybrid_flow_warning.only_add_own_account', { app_name: appName, }); - const phoneVerifyItemText = t('doc_auth.hybrid_flow_warning.only_add_phone_verify_html', { + const phoneVerifyItemText = t('doc_auth.hybrid_flow_warning.only_add_phone_verify', { app_name: appName, }); let spServicesItemText; diff --git a/app/javascript/packages/webauthn/is-webauthn-platform-supported.spec.ts b/app/javascript/packages/webauthn/is-webauthn-platform-supported.spec.ts deleted file mode 100644 index 5e1957f6c5c..00000000000 --- a/app/javascript/packages/webauthn/is-webauthn-platform-supported.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useDefineProperty } from '@18f/identity-test-helpers'; -import isWebauthnPlatformSupported from './is-webauthn-platform-supported'; - -describe('isWebauthnPlatformSupported', () => { - const defineProperty = useDefineProperty(); - - context('browser does not support webauthn', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: undefined, - }); - }); - - it('resolves to false', async () => { - await expect(isWebauthnPlatformSupported()).to.eventually.equal(false); - }); - }); - - context('browser supports webauthn', () => { - context('device does not have platform authenticator available', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(false) }, - }); - }); - - it('resolves to false', async () => { - await expect(isWebauthnPlatformSupported()).to.eventually.equal(false); - }); - }); - - context('device has platform authenticator available', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(true) }, - }); - }); - - it('resolves to true', async () => { - await expect(isWebauthnPlatformSupported()).to.eventually.equal(true); - }); - }); - }); -}); diff --git a/app/javascript/packages/webauthn/is-webauthn-platform-supported.ts b/app/javascript/packages/webauthn/is-webauthn-platform-supported.ts deleted file mode 100644 index fa52218e416..00000000000 --- a/app/javascript/packages/webauthn/is-webauthn-platform-supported.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type IsWebauthnPlatformSupported = () => Promise; - -const isWebauthnPlatformSupported: IsWebauthnPlatformSupported = async () => - !!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable()); - -export default isWebauthnPlatformSupported; diff --git a/app/javascript/packages/webauthn/webauth-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts similarity index 62% rename from app/javascript/packages/webauthn/webauth-input-element.spec.ts rename to app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 89816331c9c..ef8df395b59 100644 --- a/app/javascript/packages/webauthn/webauth-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,9 +1,7 @@ import sinon from 'sinon'; import quibble from 'quibble'; -import { waitFor } from '@testing-library/dom'; import type { IsWebauthnSupported } from './is-webauthn-supported'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; -import type { IsWebauthnPlatformSupported } from './is-webauthn-platform-supported'; describe('WebauthnInputElement', () => { const isWebauthnSupported = sinon.stub< @@ -14,15 +12,10 @@ describe('WebauthnInputElement', () => { Parameters, ReturnType >(); - const isWebauthnPlatformSupported = sinon.stub< - Parameters, - ReturnType - >(); before(async () => { quibble('./is-webauthn-supported', isWebauthnSupported); quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); - quibble('./is-webauthn-platform-supported', isWebauthnPlatformSupported); await import('./webauthn-input-element'); }); @@ -30,9 +23,7 @@ describe('WebauthnInputElement', () => { isWebauthnSupported.reset(); isWebauthnSupported.returns(false); isWebauthnPasskeySupported.reset(); - isWebauthnPasskeySupported.resolves(false); - isWebauthnPlatformSupported.reset(); - isWebauthnPlatformSupported.resolves(false); + isWebauthnPasskeySupported.returns(false); }); after(() => { @@ -62,70 +53,50 @@ describe('WebauthnInputElement', () => { document.body.innerHTML = ``; }); - it('becomes visible', async () => { + it('becomes visible', () => { const element = document.querySelector('lg-webauthn-input')!; - await waitFor(() => expect(element.hidden).to.be.false()); + expect(element.hidden).to.be.false(); }); }); context('input for platform authenticator', () => { - context('device does not have available platform authenticator', () => { - beforeEach(() => { - isWebauthnPlatformSupported.resolves(false); - document.body.innerHTML = ``; - }); - - it('stays hidden', async () => { - const element = document.querySelector('lg-webauthn-input')!; - - await waitFor(() => expect(element.isInitialized).to.be.true()); - - expect(element.hidden).to.be.true(); - }); - }); - - context('device has available platform authenticator', () => { + context('no passkey only restriction', () => { beforeEach(() => { - isWebauthnPlatformSupported.resolves(true); document.body.innerHTML = ``; }); - it('becomes visible', async () => { + it('becomes visible', () => { const element = document.querySelector('lg-webauthn-input')!; - await waitFor(() => expect(element.hidden).to.be.false()); + expect(element.hidden).to.be.false(); }); }); context('passkey supported only', () => { context('device does not support passkey', () => { beforeEach(() => { - isWebauthnPlatformSupported.resolves(true); isWebauthnPasskeySupported.returns(false); document.body.innerHTML = ``; }); - it('stays hidden', async () => { + it('stays hidden', () => { const element = document.querySelector('lg-webauthn-input')!; - await waitFor(() => expect(element.isInitialized).to.be.true()); - expect(element.hidden).to.be.true(); }); }); context('device supports passkey', () => { beforeEach(() => { - isWebauthnPlatformSupported.resolves(true); isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); - it('becomes visible', async () => { + it('becomes visible', () => { const element = document.querySelector('lg-webauthn-input')!; - await waitFor(() => expect(element.hidden).to.be.false()); + expect(element.hidden).to.be.false(); }); }); }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 5d7e5327d09..640206bafd3 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,13 +1,9 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; -import isWebauthnPlatformSupported from './is-webauthn-platform-supported'; import isWebauthnSupported from './is-webauthn-supported'; export class WebauthnInputElement extends HTMLElement { - isInitialized = false; - - async connectedCallback() { - await this.toggleVisibleIfSupported(); - this.isInitialized = true; + connectedCallback() { + this.toggleVisibleIfSupported(); } get isPlatform(): boolean { @@ -18,24 +14,16 @@ export class WebauthnInputElement extends HTMLElement { return this.hasAttribute('passkey-supported-only'); } - async isSupported(): Promise { + isSupported(): boolean { if (!isWebauthnSupported()) { return false; } - if (!this.isPlatform) { - return true; - } - - if (!(await isWebauthnPlatformSupported())) { - return false; - } - - return !this.isOnlyPasskeySupported || isWebauthnPasskeySupported(); + return !this.isPlatform || !this.isOnlyPasskeySupported || isWebauthnPasskeySupported(); } - async toggleVisibleIfSupported() { - if (await this.isSupported()) { + toggleVisibleIfSupported() { + if (this.isSupported()) { this.removeAttribute('hidden'); } } diff --git a/app/javascript/packs/pw-strength.js b/app/javascript/packs/pw-strength.js index a24e31ae9ea..54143a7ce8c 100644 --- a/app/javascript/packs/pw-strength.js +++ b/app/javascript/packs/pw-strength.js @@ -26,13 +26,17 @@ function getStrength(z) { return z && z.password.length ? scale[z.score] : fallback; } -export function getFeedback(z, minPasswordLength) { +export function getFeedback(z, minPasswordLength, forbiddenPasswords) { if (!z || !z.password) { return ' '; } const { warning, suggestions } = z.feedback; + if (forbiddenPasswords.includes(z.password)) { + return t('errors.attributes.password.avoid_using_phrases_that_are_easily_guessed'); + } + if (!warning && !suggestions.length) { if (z.password.length < minPasswordLength) { return t('errors.attributes.password.too_short.other', { count: minPasswordLength }); @@ -115,7 +119,7 @@ function validatePasswordField(score, input) { function checkPasswordStrength(password, minPasswordLength, forbiddenPasswords, input) { const z = zxcvbn(password, forbiddenPasswords); const [cls, strength] = getStrength(z); - const feedback = getFeedback(z, minPasswordLength); + const feedback = getFeedback(z, minPasswordLength, forbiddenPasswords); validatePasswordField(z.score, input); updatePasswordFeedback(cls, strength, feedback); diff --git a/app/javascript/packs/webauthn-authenticate.ts b/app/javascript/packs/webauthn-authenticate.ts index 5675e9bc3af..f98180d9677 100644 --- a/app/javascript/packs/webauthn-authenticate.ts +++ b/app/javascript/packs/webauthn-authenticate.ts @@ -6,12 +6,6 @@ function webauthn() { const webauthnSuccessContainer = document.getElementById('webauthn-auth-successful')!; const webauthAlertContainer = document.querySelector('.usa-alert--error')!; - const webauthnPlatformRequested = - webauthnInProgressContainer.dataset.platformAuthenticatorRequested === 'true'; - const multipleFactorsEnabled = - webauthnInProgressContainer.dataset.multipleFactorsEnabled === 'true'; - const isPlatformAvailable = - (document.getElementById('webauthn_device') as HTMLInputElement).value === 'true'; const spinner = document.getElementById('spinner')!; spinner.classList.remove('display-none'); @@ -20,10 +14,7 @@ function webauthn() { (document.getElementById('credentials') as HTMLInputElement).value, ); - if ( - !isWebauthnSupported() || - (webauthnPlatformRequested && !isPlatformAvailable && !multipleFactorsEnabled) - ) { + if (!isWebauthnSupported()) { const href = webauthnInProgressContainer.getAttribute('data-webauthn-not-enabled-url')!; window.location.href = href; } else { @@ -48,8 +39,6 @@ function webauthn() { }) .catch((error: Error) => { (document.getElementById('webauthn_error') as HTMLInputElement).value = error.name; - (document.getElementById('platform') as HTMLInputElement).value = - String(webauthnPlatformRequested); (document.getElementById('webauthn_form') as HTMLFormElement).submit(); }); } @@ -60,13 +49,4 @@ function webauthnButton() { button.addEventListener('click', webauthn); } -function isPlatformAuthenticatorAvailable() { - return window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable().then( - (result) => { - (document.getElementById('webauthn_device') as HTMLInputElement).value = String(result); - }, - ); -} - document.addEventListener('DOMContentLoaded', webauthnButton); -document.addEventListener('DOMContentLoaded', isPlatformAuthenticatorAvailable); diff --git a/app/jobs/usps_auth_token_refresh_job.rb b/app/jobs/usps_auth_token_refresh_job.rb new file mode 100644 index 00000000000..220fe7db19e --- /dev/null +++ b/app/jobs/usps_auth_token_refresh_job.rb @@ -0,0 +1,30 @@ +class UspsAuthTokenRefreshJob < ApplicationJob + queue_as :default + + def perform + analytics.idv_usps_auth_token_refresh_job_started + + usps_proofer.retrieve_token! + ensure + analytics.idv_usps_auth_token_refresh_job_completed + end + + private + + def usps_proofer + if IdentityConfig.store.usps_mock_fallback + UspsInPersonProofing::Mock::Proofer.new + else + UspsInPersonProofing::Proofer.new + end + end + + def analytics + @analytics ||= Analytics.new( + user: AnonymousUser.new, + request: nil, + session: {}, + sp: nil, + ) + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index d4c618b6b6c..add97797780 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -376,6 +376,12 @@ def account_rejected end end + def suspended_create_account + with_user_locale(user) do + mail(to: email_address.email, subject: t('user_mailer.suspended_create_account.subject')) + end + end + def suspended_reset_password with_user_locale(user) do mail(to: email_address.email, subject: t('user_mailer.suspended_reset_password.subject')) diff --git a/app/presenters/idv/cancellations_presenter.rb b/app/presenters/idv/cancellations_presenter.rb index e7a10fd3adc..0990eb9d8c6 100644 --- a/app/presenters/idv/cancellations_presenter.rb +++ b/app/presenters/idv/cancellations_presenter.rb @@ -26,7 +26,7 @@ def exit_description 'idv.cancel.description.exit.with_sp_html', app_name: APP_NAME, sp_name: sp_name, - account_page_link: link_to(t('idv.cancel.description.account_page'), account_path), + account_page_link_html: link_to(t('idv.cancel.description.account_page'), account_path), ) else t( diff --git a/app/presenters/idv/otp_verification_presenter.rb b/app/presenters/idv/otp_verification_presenter.rb index 998b805344c..0dc0c25afcf 100644 --- a/app/presenters/idv/otp_verification_presenter.rb +++ b/app/presenters/idv/otp_verification_presenter.rb @@ -13,7 +13,7 @@ def initialize(idv_session:) def phone_number_message t( "instructions.mfa.#{otp_delivery_preference}.number_message_html", - number: content_tag(:strong, phone_number), + number_html: content_tag(:strong, phone_number), expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, ) end diff --git a/app/presenters/piv_cac_authentication_login_presenter.rb b/app/presenters/piv_cac_authentication_login_presenter.rb index 4df6326718e..a78cfdbb1e3 100644 --- a/app/presenters/piv_cac_authentication_login_presenter.rb +++ b/app/presenters/piv_cac_authentication_login_presenter.rb @@ -26,6 +26,6 @@ def heading end def info - t('instructions.mfa.piv_cac.sign_in', app_name: APP_NAME) + t('instructions.mfa.piv_cac.sign_in_html', app_name: APP_NAME) end end diff --git a/app/presenters/piv_cac_error_presenter.rb b/app/presenters/piv_cac_error_presenter.rb index d8eacd08ceb..f41c2388152 100644 --- a/app/presenters/piv_cac_error_presenter.rb +++ b/app/presenters/piv_cac_error_presenter.rb @@ -48,7 +48,7 @@ def heading def description case error when 'piv_cac.already_associated' - t('instructions.mfa.piv_cac.already_associated_html', try_again: try_again_link) + t('instructions.mfa.piv_cac.already_associated_html', try_again_html: try_again_link) when 'user.not_found' t( 'instructions.mfa.piv_cac.account_not_found_html', @@ -57,13 +57,13 @@ def description create_account: @view.link_to(t('links.create_account'), sign_up_email_url), ) when 'certificate.none' - t('instructions.mfa.piv_cac.no_certificate_html', try_again: try_again_link) + t('instructions.mfa.piv_cac.no_certificate_html', try_again_html: try_again_link) when 'certificate.not_auth_cert' - t('instructions.mfa.piv_cac.not_auth_cert_html', please_try_again: please_try_again_link) + t('instructions.mfa.piv_cac.not_auth_cert_html', please_try_again_html: please_try_again_link) when 'token.http_failure' t('instructions.mfa.piv_cac.http_failure') else - t('instructions.mfa.piv_cac.did_not_work_html', please_try_again: please_try_again_link) + t('instructions.mfa.piv_cac.did_not_work_html', please_try_again_html: please_try_again_link) end end diff --git a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb index 79a87915b49..98f6fbcfb9a 100644 --- a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb @@ -7,7 +7,7 @@ def header def help_text t( 'instructions.mfa.authenticator.confirm_code_html', - app_name: content_tag(:strong, APP_NAME), + app_name_html: content_tag(:strong, APP_NAME), ) end diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb index 4d300a165ab..ecd2fb45be3 100644 --- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb @@ -18,7 +18,7 @@ def header def phone_number_message t( "instructions.mfa.#{otp_delivery_preference}.number_message_html", - number: content_tag(:strong, phone_number), + number_html: content_tag(:strong, phone_number), expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, ) end diff --git a/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb b/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb index 84c22f3caaf..deaefaaf425 100644 --- a/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb +++ b/app/presenters/two_factor_auth_code/piv_cac_authentication_presenter.rb @@ -9,12 +9,12 @@ def header def piv_cac_help if service_provider_mfa_policy.phishing_resistant_required? && service_provider_mfa_policy.allow_user_to_switch_method? - t('instructions.mfa.piv_cac.confirm_piv_cac_or_aal3_html') + t('instructions.mfa.piv_cac.confirm_piv_cac_or_aal3') elsif service_provider_mfa_policy.phishing_resistant_required? || service_provider_mfa_policy.piv_cac_required? - t('instructions.mfa.piv_cac.confirm_piv_cac_only_html') + t('instructions.mfa.piv_cac.confirm_piv_cac_only') else - t('instructions.mfa.piv_cac.confirm_piv_cac_html') + t('instructions.mfa.piv_cac.confirm_piv_cac') end end diff --git a/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb b/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb index cbb72891f3c..7fd4dfc6969 100644 --- a/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb +++ b/app/presenters/two_factor_auth_code/webauthn_authentication_presenter.rb @@ -19,13 +19,13 @@ def initialize(data:, view:, service_provider:, remember_device_default: true, def webauthn_help if service_provider_mfa_policy.phishing_resistant_required? && service_provider_mfa_policy.allow_user_to_switch_method? - t('instructions.mfa.webauthn.confirm_webauthn_or_aal3_html') + t('instructions.mfa.webauthn.confirm_webauthn_or_aal3') elsif service_provider_mfa_policy.phishing_resistant_required? - t('instructions.mfa.webauthn.confirm_webauthn_only_html') + t('instructions.mfa.webauthn.confirm_webauthn_only') elsif platform_authenticator? - t('instructions.mfa.webauthn.confirm_webauthn_platform_html', app_name: APP_NAME) + t('instructions.mfa.webauthn.confirm_webauthn_platform', app_name: APP_NAME) else - t('instructions.mfa.webauthn.confirm_webauthn_html') + t('instructions.mfa.webauthn.confirm_webauthn') end end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index 3b8dde9a926..c46221f3cbe 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -76,7 +76,7 @@ def first_enabled_option_index def account_reset_link t( 'two_factor_authentication.account_reset.text_html', - link: @view.link_to( + link_html: @view.link_to( t('two_factor_authentication.account_reset.link'), account_reset_url(locale: LinkLocaleResolver.locale), ), @@ -88,12 +88,15 @@ def account_reset_url(locale:) end def account_reset_cancel_link - t( - 'two_factor_authentication.account_reset.pending_html', - cancel_link: @view.link_to( - t('two_factor_authentication.account_reset.cancel_link'), - account_reset_cancel_url(token: account_reset_token), - ), + safe_join( + [ + t('two_factor_authentication.account_reset.pending'), + @view.link_to( + t('two_factor_authentication.account_reset.cancel_link'), + account_reset_cancel_url(token: account_reset_token), + ), + ], + ' ', ) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 4c5c08d0625..fc73d164e6f 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -2209,6 +2209,22 @@ def idv_start_over( ) end + # Track when USPS auth token refresh job completed + def idv_usps_auth_token_refresh_job_completed(**extra) + track_event( + 'UspsAuthTokenRefreshJob: Completed', + **extra, + ) + end + + # Track when USPS auth token refresh job started + def idv_usps_auth_token_refresh_job_started(**extra) + track_event( + 'UspsAuthTokenRefreshJob: Started', + **extra, + ) + end + # @param [String] flow_path Document capture path ("hybrid" or "standard") # The user clicked the troubleshooting option to start in-person proofing def idv_verify_in_person_troubleshooting_option_clicked(flow_path:, @@ -2302,6 +2318,7 @@ def logout_initiated( # @param [Hash] errors Authentication error reasons, if unsuccessful # @param [String] context # @param [String] multi_factor_auth_method + # @param [DateTime] multi_factor_auth_method_created_at time auth method was created # @param [Integer] auth_app_configuration_id # @param [Integer] piv_cac_configuration_id # @param [Integer] key_id @@ -2317,6 +2334,7 @@ def multi_factor_auth( errors: nil, context: nil, multi_factor_auth_method: nil, + multi_factor_auth_method_created_at: nil, auth_app_configuration_id: nil, piv_cac_configuration_id: nil, key_id: nil, @@ -2335,6 +2353,7 @@ def multi_factor_auth( errors: errors, context: context, multi_factor_auth_method: multi_factor_auth_method, + multi_factor_auth_method_created_at: multi_factor_auth_method_created_at, auth_app_configuration_id: auth_app_configuration_id, piv_cac_configuration_id: piv_cac_configuration_id, key_id: key_id, @@ -3876,10 +3895,12 @@ def user_suspended( # Tracks when USPS in-person proofing enrollment is created # @param [String] enrollment_code # @param [Integer] enrollment_id + # @param [Boolean] second_address_line_present # @param [String] service_provider def usps_ippaas_enrollment_created( enrollment_code:, enrollment_id:, + second_address_line_present:, service_provider:, **extra ) @@ -3887,6 +3908,7 @@ def usps_ippaas_enrollment_created( 'USPS IPPaaS enrollment created', enrollment_code: enrollment_code, enrollment_id: enrollment_id, + second_address_line_present: second_address_line_present, service_provider: service_provider, **extra, ) diff --git a/app/services/backup_code_generator.rb b/app/services/backup_code_generator.rb index c893756cfd3..b8683674a39 100644 --- a/app/services/backup_code_generator.rb +++ b/app/services/backup_code_generator.rb @@ -24,12 +24,17 @@ def create # @return [Boolean] def verify(plaintext_code) - return false unless plaintext_code.present? + if_valid_consume_code_return_config(plaintext_code).present? + end + + # @return [BackupCodeConfiguration, nil] + def if_valid_consume_code_return_config(plaintext_code) + return unless plaintext_code.present? backup_code = RandomPhrase.normalize(plaintext_code) - code = BackupCodeConfiguration.find_with_code(code: backup_code, user_id: @user.id) - return false unless code_usable?(code) - code.update!(used_at: Time.zone.now) - true + config = BackupCodeConfiguration.find_with_code(code: backup_code, user_id: @user.id) + return unless code_usable?(config) + config.update!(used_at: Time.zone.now) + config end def delete_existing_codes diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb index 75cc2b6707a..21ddd0ea7db 100644 --- a/app/services/saml_request_validator.rb +++ b/app/services/saml_request_validator.rb @@ -39,7 +39,9 @@ def authorized_service_provider def authorized_authn_context if !valid_authn_context? || - (ial2_context_requested? && service_provider&.ial != 2) + (ial2_context_requested? && service_provider&.ial != 2) || + (ial_max_requested? && + !IdentityConfig.store.allowed_ialmax_providers.include?(service_provider&.issuer)) errors.add(:authn_context, :unauthorized_authn_context, type: :unauthorized_authn_context) end end @@ -71,6 +73,10 @@ def ial2_context_requested? end end + def ial_max_requested? + Array(authn_context).include?(Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF) + end + def authorized_email_nameid_format return unless email_nameid_format? return if service_provider&.email_nameid_format_allowed diff --git a/app/services/send_sign_up_email_confirmation.rb b/app/services/send_sign_up_email_confirmation.rb index 6c983d4b1c4..0e06e9821cb 100644 --- a/app/services/send_sign_up_email_confirmation.rb +++ b/app/services/send_sign_up_email_confirmation.rb @@ -10,7 +10,9 @@ def initialize(user) def call(request_id: nil, instructions: nil, password_reset_requested: false) update_email_address_record - if password_reset_requested && !user.confirmed? + if user.suspended? + send_suspended_user_email + elsif password_reset_requested && !user.confirmed? send_pw_reset_request_unconfirmed_user_email(request_id, instructions) else send_confirmation_email(request_id, instructions) @@ -67,6 +69,13 @@ def send_pw_reset_request_unconfirmed_user_email(request_id, instructions) ).deliver_now_or_later end + def send_suspended_user_email + UserMailer.with( + user: user, + email_address: email_address, + ).suspended_create_account.deliver_now_or_later + end + def handle_multiple_email_address_error raise 'sign up user has multiple email address records' end diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb index 88e25feef70..e5758ac122e 100644 --- a/app/services/usps_in_person_proofing/enrollment_helper.rb +++ b/app/services/usps_in_person_proofing/enrollment_helper.rb @@ -8,6 +8,14 @@ def schedule_in_person_enrollment(user, pii) enrollment.current_address_matches_id = pii['same_address_as_id'] enrollment.save! + # If we're using secondary ID capture (aka double address verification), + # then send the state ID address to USPS. Otherwise send the residential address. + pii = pii.to_h + if enrollment.capture_secondary_id_enabled? && !enrollment.current_address_matches_id? + pii = pii.except(*SECONDARY_ID_ADDRESS_MAP.values). + transform_keys(SECONDARY_ID_ADDRESS_MAP) + end + enrollment_code = create_usps_enrollment(enrollment, pii) return unless enrollment_code @@ -20,6 +28,7 @@ def schedule_in_person_enrollment(user, pii) analytics(user: user).usps_ippaas_enrollment_created( enrollment_code: enrollment.enrollment_code, enrollment_id: enrollment.id, + second_address_line_present: pii[:address2].present?, service_provider: enrollment.service_provider&.issuer, ) @@ -44,23 +53,13 @@ def create_usps_enrollment(enrollment, pii) # Use the enrollment's unique_id value if it exists, otherwise use the deprecated # #usps_unique_id value in order to remain backwards-compatible. LG-7024 will remove this unique_id = enrollment.unique_id || enrollment.usps_unique_id - pii = pii.to_h - - # If we're using secondary ID capture (aka double address verification), - # then send the state ID address to USPS. Otherwise send the residential address. - if enrollment.capture_secondary_id_enabled? && !enrollment.current_address_matches_id? - pii = pii.except(*SECONDARY_ID_ADDRESS_MAP.values). - transform_keys(SECONDARY_ID_ADDRESS_MAP) - end - - address = [pii[:address1], pii[:address2]].select(&:present?).join(' ') applicant = UspsInPersonProofing::Applicant.new( { unique_id: unique_id, first_name: transliterate(pii[:first_name]), last_name: transliterate(pii[:last_name]), - address: transliterate(address), + address: transliterate(pii[:address1]), city: transliterate(pii[:city]), state: pii[:state], zip_code: pii[:zipcode], diff --git a/app/views/account_reset/confirm_delete_account/show.html.erb b/app/views/account_reset/confirm_delete_account/show.html.erb index e37d4f48331..e313062f73c 100644 --- a/app/views/account_reset/confirm_delete_account/show.html.erb +++ b/app/views/account_reset/confirm_delete_account/show.html.erb @@ -9,7 +9,7 @@

<%= t( 'account_reset.confirm_delete_account.cta_html', - link: link_to( + link_html: link_to( t('account_reset.confirm_delete_account.link_text'), sign_up_email_path, ), diff --git a/app/views/accounts/_identity_item.html.erb b/app/views/accounts/_identity_item.html.erb index b4722b99094..953bd46d01c 100644 --- a/app/views/accounts/_identity_item.html.erb +++ b/app/views/accounts/_identity_item.html.erb @@ -3,7 +3,7 @@ <% if event.return_to_sp_url.present? %> <%= t( 'event_types.authenticated_at_html', - service_provider_link: link_to(event.display_name, event.return_to_sp_url), + service_provider_link_html: link_to(event.display_name, event.return_to_sp_url), ) %> <% else %> <%= t('event_types.authenticated_at', service_provider: event.display_name) %> diff --git a/app/views/forgot_password/show.html.erb b/app/views/forgot_password/show.html.erb index c22a6d119d0..6ebf11bfa79 100644 --- a/app/views/forgot_password/show.html.erb +++ b/app/views/forgot_password/show.html.erb @@ -24,10 +24,11 @@

<%= t('notices.forgot_password.no_email_sent_explanation_start') %> <%= f.button :button, t('links.resend'), class: 'usa-button--unstyled margin-left-05' %>

- <% link = link_to( - t('notices.forgot_password.use_diff_email.link'), - sign_up_email_path, - ) %> -

<%= t('notices.forgot_password.use_diff_email.text_html', link: link) %>

+

+ <%= t( + 'notices.forgot_password.use_diff_email.text_html', + link_html: link_to(t('notices.forgot_password.use_diff_email.link'), sign_up_email_path), + ) %> +

<%= t('instructions.forgot_password.close_window') %>

<% end %> diff --git a/app/views/idv/activated.html.erb b/app/views/idv/activated.html.erb index c2647482e2c..9072c171c89 100644 --- a/app/views/idv/activated.html.erb +++ b/app/views/idv/activated.html.erb @@ -5,6 +5,6 @@

<%= t( 'idv.messages.activated_html', - link: link_to(t('idv.messages.activated_link'), MarketingSite.contact_url), + link_html: link_to(t('idv.messages.activated_link'), MarketingSite.contact_url), ) %>

diff --git a/app/views/idv/gpo_verify/index.html.erb b/app/views/idv/gpo_verify/index.html.erb index 40612a17bb5..c06ff2a0906 100644 --- a/app/views/idv/gpo_verify/index.html.erb +++ b/app/views/idv/gpo_verify/index.html.erb @@ -22,7 +22,7 @@ <% end %> <%= render PageHeadingComponent.new.with_content(t('forms.verify_profile.welcome_back')) %> -<%= sanitize t('forms.verify_profile.welcome_back_description'), tags: %w[p strong] %> +<%= t('forms.verify_profile.welcome_back_description_html') %>

<%= t('forms.verify_profile.title') %>

diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb index cb8a93c5fe7..778234e56ae 100644 --- a/app/views/idv/in_person/ready_to_verify/show.html.erb +++ b/app/views/idv/in_person/ready_to_verify/show.html.erb @@ -115,7 +115,7 @@ <%= render ClickObserverComponent.new(event_name: 'IdV: user clicked sp link on ready to verify page') do %> <%= t( 'in_person_proofing.body.barcode.return_to_partner_html', - link: link_to( + link_html: link_to( t( 'in_person_proofing.body.barcode.return_to_partner_link', sp_name: @presenter.sp_name, diff --git a/app/views/idv/in_person/verify_info/show.html.erb b/app/views/idv/in_person/verify_info/show.html.erb index b2c0c0f82ed..ea5ae8073c4 100644 --- a/app/views/idv/in_person/verify_info/show.html.erb +++ b/app/views/idv/in_person/verify_info/show.html.erb @@ -26,7 +26,7 @@ locals: ) do %> <%= t( 'doc_auth.headings.capture_scan_warning_html', - link: link_to( + link_html: link_to( t('doc_auth.headings.capture_scan_warning_link'), idv_hybrid_handoff_url(redo: true), 'aria-label': t('doc_auth.headings.capture_scan_warning_link'), diff --git a/app/views/idv/please_call/show.html.erb b/app/views/idv/please_call/show.html.erb index 2f0cd590242..1172c70fe15 100644 --- a/app/views/idv/please_call/show.html.erb +++ b/app/views/idv/please_call/show.html.erb @@ -12,9 +12,9 @@ heading: t('idv.failure.setup.heading'), ) do %>

- <%= t('idv.failure.setup.fail_date_html', date: I18n.l(@call_by_date, format: I18n.t('time.formats.event_date'))) %> + <%= t('idv.failure.setup.fail_date_html', date_html: I18n.l(@call_by_date, format: I18n.t('time.formats.event_date'))) %>

- <%= t('idv.failure.setup.fail_html', support_code: IdentityConfig.store.lexisnexis_threatmetrix_support_code, contact_number: IdentityConfig.store.idv_contact_phone_number) %> + <%= t('idv.failure.setup.fail', support_code: IdentityConfig.store.lexisnexis_threatmetrix_support_code, contact_number: IdentityConfig.store.idv_contact_phone_number) %>

<% end %> diff --git a/app/views/idv/session_errors/exception.html.erb b/app/views/idv/session_errors/exception.html.erb index bb383d220eb..57296187ddc 100644 --- a/app/views/idv/session_errors/exception.html.erb +++ b/app/views/idv/session_errors/exception.html.erb @@ -25,7 +25,7 @@

<%= t( 'idv.failure.exceptions.text_html', - link: link_to(t('idv.failure.exceptions.link'), MarketingSite.contact_url), + link_html: link_to(t('idv.failure.exceptions.link'), MarketingSite.contact_url), ) %>

<% end %> diff --git a/app/views/idv/unavailable/show.html.erb b/app/views/idv/unavailable/show.html.erb index c385a886380..9815916580a 100644 --- a/app/views/idv/unavailable/show.html.erb +++ b/app/views/idv/unavailable/show.html.erb @@ -20,7 +20,7 @@ <%= t( 'idv.unavailable.next_steps_html', app_name: APP_NAME, - status_page_link: new_tab_link_to( + status_page_link_html: new_tab_link_to( t('idv.unavailable.status_page_link'), StatusPage.base_url, ), diff --git a/app/views/idv/verify_info/show.html.erb b/app/views/idv/verify_info/show.html.erb index eab7ca997f5..f9064ceb0e6 100644 --- a/app/views/idv/verify_info/show.html.erb +++ b/app/views/idv/verify_info/show.html.erb @@ -26,7 +26,7 @@ locals: ) do %> <%= t( 'doc_auth.headings.capture_scan_warning_html', - link: link_to( + link_html: link_to( t('doc_auth.headings.capture_scan_warning_link'), idv_hybrid_handoff_url(redo: true), 'aria-label': t('doc_auth.headings.capture_scan_warning_link'), diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 771cb8c2f0a..4710132922a 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -26,7 +26,7 @@ <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.welcome')) %>

- <%= t('doc_auth.info.welcome_html', sp_name: decorated_session.sp_name || t('doc_auth.info.no_sp_name')) %> + <%= t('doc_auth.info.welcome', sp_name: decorated_session.sp_name || t('doc_auth.info.no_sp_name')) %>

<%= t('doc_auth.instructions.welcome') %>

diff --git a/app/views/partials/personal_key/_key.html.erb b/app/views/partials/personal_key/_key.html.erb index d8a62da1b9a..78ca92fc4d4 100644 --- a/app/views/partials/personal_key/_key.html.erb +++ b/app/views/partials/personal_key/_key.html.erb @@ -16,12 +16,14 @@ <% if local_assigns[:personal_key_generated_at].present? %> <%= t( 'users.personal_key.generated_on_html', - date: content_tag(:strong, render(TimeComponent.new(time: personal_key_generated_at))), + date_html: content_tag( + :strong, render(TimeComponent.new(time: personal_key_generated_at)) + ), ) %> <% else %> <%= t( 'users.personal_key.generated_on_html', - date: content_tag(:strong, render(TimeComponent.new(time: Time.zone.today))), + date_html: content_tag(:strong, render(TimeComponent.new(time: Time.zone.today))), ) %> <% end %>

diff --git a/app/views/session_timeout/_warning.html.erb b/app/views/session_timeout/_warning.html.erb index c4871223b5d..96ab50b783b 100644 --- a/app/views/session_timeout/_warning.html.erb +++ b/app/views/session_timeout/_warning.html.erb @@ -13,7 +13,7 @@ # i18n-tasks-use t('notices.timeout_warning.partially_signed_in.message_html') 'message_html', scope: modal_presenter.translation_scope, - time_left_in_session: render( + time_left_in_session_html: render( CountdownComponent.new( expiration: Time.zone.now, start_immediately: false, @@ -27,7 +27,7 @@ # i18n-tasks-use t('notices.timeout_warning.partially_signed_in.live_region_message_html') 'live_region_message_html', scope: modal_presenter.translation_scope, - time_left_in_session: render( + time_left_in_session_html: render( CountdownComponent.new( expiration: Time.zone.now, update_interval: 30.seconds, diff --git a/app/views/shared/_recaptcha_disclosure.html.erb b/app/views/shared/_recaptcha_disclosure.html.erb index 4b20ccc1b69..af839e11491 100644 --- a/app/views/shared/_recaptcha_disclosure.html.erb +++ b/app/views/shared/_recaptcha_disclosure.html.erb @@ -2,8 +2,8 @@ <%= t( 'two_factor_authentication.recaptcha.disclosure_statement_html', app_name: APP_NAME, - google_policy_link: new_tab_link_to(t('two_factor_authentication.recaptcha.google_policy_link'), GooglePolicySite.privacy_url), - google_tos_link: new_tab_link_to(t('two_factor_authentication.recaptcha.google_tos_link'), GooglePolicySite.terms_url), - login_tos_link: new_tab_link_to(t('two_factor_authentication.recaptcha.login_tos_link'), MarketingSite.rules_of_use_url), + google_policy_link_html: new_tab_link_to(t('two_factor_authentication.recaptcha.google_policy_link'), GooglePolicySite.privacy_url), + google_tos_link_html: new_tab_link_to(t('two_factor_authentication.recaptcha.google_tos_link'), GooglePolicySite.terms_url), + login_tos_link_html: new_tab_link_to(t('two_factor_authentication.recaptcha.login_tos_link'), MarketingSite.rules_of_use_url), ) %>

\ No newline at end of file diff --git a/app/views/shared/_ssn_field.html.erb b/app/views/shared/_ssn_field.html.erb index 44afe95439f..f38d714f371 100644 --- a/app/views/shared/_ssn_field.html.erb +++ b/app/views/shared/_ssn_field.html.erb @@ -10,7 +10,7 @@ locals: field_options: { name: :ssn, as: :password, - label: t('idv.form.ssn_label_html'), + label: t('idv.form.ssn_label'), hint: t('forms.example') + ' 123-45-6789', required: true, pattern: '^\d{3}-?\d{2}-?\d{4}$', diff --git a/app/views/sign_up/emails/show.html.erb b/app/views/sign_up/emails/show.html.erb index d091a907530..033e2b1948d 100644 --- a/app/views/sign_up/emails/show.html.erb +++ b/app/views/sign_up/emails/show.html.erb @@ -27,8 +27,12 @@

<%= t('notices.signed_up_but_unconfirmed.no_email_sent_explanation_start') %> <%= f.button :button, t('links.resend'), class: 'usa-button--unstyled margin-left-05' %>

- <% link = link_to(t('notices.use_diff_email.link'), sign_up_email_path) %> -

<%= t('notices.use_diff_email.text_html', link: link) %>

+

+ <%= t( + 'notices.use_diff_email.text_html', + link_html: link_to(t('notices.use_diff_email.link'), sign_up_email_path), + ) %> +

<%= t('devise.registrations.close_window') %>

<% if FeatureManagement.enable_load_testing_mode? %> diff --git a/app/views/two_factor_authentication/sms_opt_in/error.html.erb b/app/views/two_factor_authentication/sms_opt_in/error.html.erb index fa8331cb50b..85164530e04 100644 --- a/app/views/two_factor_authentication/sms_opt_in/error.html.erb +++ b/app/views/two_factor_authentication/sms_opt_in/error.html.erb @@ -5,7 +5,7 @@

<%= t( 'two_factor_authentication.opt_in.opted_out_last_30d_html', - phone_number: content_tag(:strong, @phone_configuration.masked_phone), + phone_number_html: content_tag(:strong, @phone_configuration.masked_phone), ) %>

diff --git a/app/views/two_factor_authentication/sms_opt_in/new.html.erb b/app/views/two_factor_authentication/sms_opt_in/new.html.erb index b2cd656a535..5996f678d4a 100644 --- a/app/views/two_factor_authentication/sms_opt_in/new.html.erb +++ b/app/views/two_factor_authentication/sms_opt_in/new.html.erb @@ -5,7 +5,7 @@

<%= t( 'two_factor_authentication.opt_in.opted_out_html', - phone_number: content_tag(:strong, @phone_configuration.masked_phone), + phone_number_html: content_tag(:strong, @phone_configuration.masked_phone), ) %>

diff --git a/app/views/two_factor_authentication/webauthn_verification/show.html.erb b/app/views/two_factor_authentication/webauthn_verification/show.html.erb index 3833d889ba2..c12ff6faee0 100644 --- a/app/views/two_factor_authentication/webauthn_verification/show.html.erb +++ b/app/views/two_factor_authentication/webauthn_verification/show.html.erb @@ -22,16 +22,13 @@ <%= hidden_field_tag :signature, '', id: 'signature' %> <%= hidden_field_tag :client_data_json, '', id: 'client_data_json' %> <%= hidden_field_tag :webauthn_error, '', id: 'webauthn_error' %> - <%= hidden_field_tag :platform, '', id: 'platform' %> - <%= hidden_field_tag :webauthn_device, '', id: 'webauthn_device' %> + <%= hidden_field_tag :platform, @presenter.platform_authenticator? %> <%= content_tag( :div, id: 'webauthn-auth-in-progress', data: { webauthn_not_enabled_url: @presenter.webauthn_not_enabled_link, - platform_authenticator_requested: @presenter.platform_authenticator?, - multiple_factors_enabled: @presenter.multiple_factors_enabled?, }, ) do %>