diff --git a/Gemfile b/Gemfile index a645422ddb6..6e20900ec6b 100644 --- a/Gemfile +++ b/Gemfile @@ -66,6 +66,7 @@ gem 'redacted_struct' gem 'redis', '>= 3.2.0' gem 'redis-session-store', github: '18F/redis-session-store', tag: 'v1.0.1-18f' gem 'retries' +gem 'rexml', '~> 3.3' gem 'rotp', '~> 6.3', '>= 6.3.0' gem 'rqrcode' gem 'ruby-progressbar' diff --git a/Gemfile.lock b/Gemfile.lock index 26a02875269..71f99fe949b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -573,7 +573,7 @@ GEM actionpack (>= 5.0) railties (>= 5.0) retries (0.0.5) - rexml (3.3.1) + rexml (3.3.2) strscan rotp (6.3.0) rouge (4.2.0) @@ -832,6 +832,7 @@ DEPENDENCIES redis (>= 3.2.0) redis-session-store! retries + rexml (~> 3.3) rotp (~> 6.3, >= 6.3.0) rqrcode rspec (~> 3.13.0) diff --git a/Makefile b/Makefile index eaa8c63d553..e1c37a66d90 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ lint_asset_bundle_size: ## Lints JavaScript and CSS compiled bundle size @# and you have no options to split that from the common bundles. If you need to increase this @# budget and accept the fact that this will force end-users to endure longer load times, you @# should set the new budget to within a few thousand bytes of the production-compiled size. - find app/assets/builds/application.css -size -185000c | grep . + find app/assets/builds/application.css -size -105000c | grep . find public/packs/application-*.digested.js -size -5000c | grep . lint_migrations: diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 6015355f074..86437370173 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -3,5 +3,3 @@ @forward 'uswds'; @forward 'design-system-waiting-room'; @forward 'components'; -@forward 'uswds-utilities'; -@forward 'utilities'; diff --git a/app/assets/stylesheets/utilities.css.scss b/app/assets/stylesheets/utilities.css.scss new file mode 100644 index 00000000000..724dbfd7338 --- /dev/null +++ b/app/assets/stylesheets/utilities.css.scss @@ -0,0 +1,3 @@ +@forward 'uswds-core'; +@forward 'uswds-utilities'; +@forward 'utilities'; diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index b071093cf9c..b7799d71181 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -10,6 +10,7 @@ class RequestLetterController < ApplicationController before_action :confirm_mail_not_rate_limited before_action :confirm_step_allowed + before_action :confirm_letter_sends_allowed def index @applicant = idv_session.applicant @@ -33,7 +34,7 @@ def self.step_info action: :index, next_steps: [:enter_password], preconditions: ->(idv_session:, user:) do - idv_session.verify_info_step_complete? || user.gpo_verification_pending_profile? + idv_session.verify_info_step_complete? end, undo_step: ->(idv_session:, user:) { idv_session.address_verification_mechanism = nil }, ) @@ -55,6 +56,10 @@ def confirm_mail_not_rate_limited redirect_to idv_enter_password_url if gpo_verify_by_mail_policy.rate_limited? end + def confirm_letter_sends_allowed + redirect_to idv_enter_password_url if !gpo_verify_by_mail_policy.send_letter_available? + end + def step_indicator_steps if in_person_proofing? Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS_GPO diff --git a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb index 16c6733047f..99e37c8ad16 100644 --- a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb +++ b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb @@ -2,85 +2,20 @@ module Users class PivCacSetupFromSignInController < ApplicationController - include TwoFactorAuthenticatableMethods include PivCacConcern - include SecureHeadersConcern include ReauthenticationRequiredConcern before_action :confirm_two_factor_authenticated before_action :confirm_recently_authenticated_2fa - before_action :apply_secure_headers_override, only: :success before_action :set_piv_cac_setup_csp_form_action_uris, only: :prompt def prompt - if params.key?(:token) - process_piv_cac_setup - else - render_prompt - end - end - - def success; end - - def next - redirect_to after_sign_in_path_for(current_user) + analytics.piv_cac_setup_visited(in_account_creation_flow: false) end def decline session.delete(:needs_to_setup_piv_cac_after_sign_in) redirect_to after_sign_in_path_for(current_user) end - - private - - def render_prompt - analytics.piv_cac_setup_visited(in_account_creation_flow: false) - render :prompt - end - - def process_piv_cac_setup - result = user_piv_cac_form.submit - properties = result.to_h.merge(analytics_properties) - analytics.multi_factor_auth_setup(**properties) - if result.success? - process_valid_submission - else - process_invalid_submission - end - end - - def user_piv_cac_form - @user_piv_cac_form ||= UserPivCacSetupForm.new( - user: current_user, - token: params[:token], - nonce: piv_cac_nonce, - name: user_session[:piv_cac_nickname], - ) - end - - def process_invalid_submission - redirect_to login_piv_cac_error_url(error: user_piv_cac_form.error_type) - end - - def process_valid_submission - handle_valid_verification_for_confirmation_context( - auth_method: TwoFactorAuthenticatable::AuthMethod::PIV_CAC, - ) - session.delete(:needs_to_setup_piv_cac_after_sign_in) - save_piv_cac_information( - subject: user_piv_cac_form.x509_dn, - issuer: user_piv_cac_form.x509_issuer, - presented: true, - ) - create_user_event(:piv_cac_enabled) - redirect_to login_add_piv_cac_success_url - end - - def analytics_properties - { - in_account_creation_flow: false, - enabled_mfa_methods_count: MfaContext.new(current_user).enabled_mfa_methods_count, - } - end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 159e784baf2..ffc8d858bc0 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -176,6 +176,7 @@ def track_authentication_attempt(email) bad_password_count: session[:bad_password_count].to_i, sp_request_url_present: sp_session[:request_url].present?, remember_device: remember_device_cookie.present?, + new_device: success ? new_device? : nil, ) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index e68db8eb358..bb396286a23 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -404,6 +404,8 @@ def edit_password_visit # @param [String] bad_password_count represents number of prior login failures # @param [Boolean] sp_request_url_present if was an SP request URL in the session # @param [Boolean] remember_device if the remember device cookie was present + # @param [Boolean, nil] new_device Whether the user is authenticating from a new device. Nil if + # there is the attempt was unsuccessful, since it cannot be known whether it's a new device. # Tracks authentication attempts at the email/password screen def email_and_password_auth( success:, @@ -413,6 +415,7 @@ def email_and_password_auth( bad_password_count:, sp_request_url_present:, remember_device:, + new_device:, **extra ) track_event( @@ -424,6 +427,7 @@ def email_and_password_auth( bad_password_count:, sp_request_url_present:, remember_device:, + new_device:, **extra, ) end @@ -670,6 +674,25 @@ def fraud_review_rejected( ) end + # @param [Boolean] success Whether records were successfully uploaded + # @param [String] exception The exception that occured if an exception did occur + # @param [Number] gpo_confirmation_count The number of GPO Confirmation records uploaded + # GPO confirmation records were uploaded for letter sends + def gpo_confirmation_upload( + success:, + exception:, + gpo_confirmation_count:, + **extra + ) + track_event( + :gpo_confirmation_upload, + success: success, + exception: exception, + gpo_confirmation_count: gpo_confirmation_count, + **extra, + ) + end + # @param [Boolean] acuant_sdk_upgrade_a_b_testing_enabled # @param [String] acuant_version # @param [String] flow_path whether the user is in the hybrid or standard flow @@ -4125,12 +4148,17 @@ def multi_factor_auth_added_phone( # Tracks when the user has added the MFA method piv_cac to their account # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user # @param [Boolean] in_account_creation_flow whether user is going through creation flow - def multi_factor_auth_added_piv_cac(enabled_mfa_methods_count:, in_account_creation_flow:, - **extra) + # @param ['piv_cac'] method_name Authentication method added + def multi_factor_auth_added_piv_cac( + enabled_mfa_methods_count:, + in_account_creation_flow:, + method_name: :piv_cac, + **extra + ) track_event( :multi_factor_auth_added_piv_cac, { - method_name: :piv_cac, + method_name:, enabled_mfa_methods_count:, in_account_creation_flow:, **extra, diff --git a/app/services/gpo_confirmation_uploader.rb b/app/services/gpo_confirmation_uploader.rb index 54422aba58c..4f16d983a6c 100644 --- a/app/services/gpo_confirmation_uploader.rb +++ b/app/services/gpo_confirmation_uploader.rb @@ -12,7 +12,13 @@ def run upload_export(export) LetterRequestsToGpoFtpLog.create(ftp_at: @now, letter_requests_count: confirmations.count) clear_confirmations(confirmations) + analytics.gpo_confirmation_upload( + success: true, exception: nil, gpo_confirmation_count: confirmations.count, + ) rescue StandardError => error + analytics.gpo_confirmation_upload( + success: false, exception: error.to_s, gpo_confirmation_count: 0, + ) NewRelic::Agent.notice_error(error) raise error end @@ -71,4 +77,8 @@ def sftp_config timeout: IdentityConfig.store.usps_upload_sftp_timeout, ] end + + def analytics + Analytics.new(user: AnonymousUser.new, request: nil, session: {}, sp: nil) + end end diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 2ec6e6e4247..648cf73d3a0 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -20,8 +20,9 @@ <% end %> <%= preload_link_tag font_path('public-sans/PublicSans-Bold.woff2') %> <%= preload_link_tag font_path('public-sans/PublicSans-Regular.woff2') %> - <%= render_stylesheet_once_tags %> <%= stylesheet_link_tag 'application', nopush: false %> + <%= render_stylesheet_once_tags %> + <%= stylesheet_link_tag 'utilities', nopush: false %> <%= stylesheet_link_tag 'print', media: :print, preload_links_header: false %> <%= csrf_meta_tags %> diff --git a/app/views/layouts/component_preview.html.erb b/app/views/layouts/component_preview.html.erb index d5662395c30..90344bed5f9 100644 --- a/app/views/layouts/component_preview.html.erb +++ b/app/views/layouts/component_preview.html.erb @@ -2,8 +2,9 @@
就像解锁你的设备一样来做身份证实,无论是用你的面孔还是指纹、密码或\ - 者其他方法。
如果你使用iCloud Keychain 或 Google Password Manager 这样的密码管理器,管理器可能会要保存一个通行密钥。这会使你能使用那个密码管理器在任何设备上证实身份。%{link}
" +forms.webauthn_platform_setup.intro_html: '就像解锁你的设备一样来做身份证实,无论是用你的面孔还是指纹、密码或者其他方法。
如果你使用iCloud Keychain 或 Google Password Manager 这样的密码管理器,管理器可能会要保存一个通行密钥。这会使你能使用那个密码管理器在任何设备上证实身份。%{link}
' forms.webauthn_platform_setup.intro_link_text: 了解关于使用不同设备的更多信息。 forms.webauthn_platform_setup.nickname: 设备昵称 forms.webauthn_platform_setup.nickname_hint: 这样如果你添加了更多使用人脸或触摸解锁的设备的话,你就能把它们分辨开来。 @@ -971,8 +967,7 @@ help_text.requested_attributes.email: 电邮地址 help_text.requested_attributes.full_name: 姓名 help_text.requested_attributes.ial1_consent_reminder_html: 你每年都必须授权同意与 %{sp} 分享信息。我们将与 %{sp} 分享你的信息来连接你账户。 help_text.requested_attributes.ial1_intro_html: 我们将与 %{sp} 分享你的信息来连接你账户。 -help_text.requested_attributes.ial2_consent_reminder_html: "%{sp} 需要知道你是谁才能连接你的账户。你每年都必须授权同意与 %{sp} 分享已验证过的你的信息。我们会分享这些信息:" +help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} 需要知道你是谁才能连接你的账户。你每年都必须授权同意与 %{sp} 分享已验证过的你的信息。我们会分享这些信息:' help_text.requested_attributes.ial2_intro_html: '%{sp} 需要知道你是谁才能连接你的账户。我们会与 %{sp} 分享这些信息:' help_text.requested_attributes.ial2_reverified_consent_info: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp} 分享该信息。' help_text.requested_attributes.phone: 电话号码 @@ -1356,8 +1351,7 @@ instructions.mfa.sms.number_message_html: 我们把带有一次性代码的短 instructions.mfa.voice.number_message_html: 我们给 %{number_html}打了电话告知一次性代码。这一代码 %{expiration} 分钟后会作废。 instructions.mfa.webauthn_platform.learn_more_help: 了解更多有关人脸或触摸解锁的信息 instructions.mfa.webauthn.confirm_webauthn: 提供与你账户相关的安全密钥。 -instructions.mfa.webauthn.confirm_webauthn_platform_html: "你可以像解锁你的设备一样来做身份证\ - 实,无论是用你的面孔还是指纹、密码或者其他方法。
如果你用一个密码管理器设置了人脸或触摸解锁,则可以从使用那一密码管理器的任何设备进行身份证实。否则的话,使用你设置人脸或触摸解锁的同一设备。
" +instructions.mfa.webauthn.confirm_webauthn_platform_html: '你可以像解锁你的设备一样来做身份证实,无论是用你的面孔还是指纹、密码或者其他方法。
如果你用一个密码管理器设置了人脸或触摸解锁,则可以从使用那一密码管理器的任何设备进行身份证实。否则的话,使用你设置人脸或触摸解锁的同一设备。
' instructions.mfa.wrong_number: 输入的电话号码不对? instructions.password.forgot: 不知道你的密码?确认你的电邮地址后重设密码。 instructions.password.help_text: 避免重复使用你其他网上账户(比如银行、电邮和社交媒体)的密码。请勿包括你电邮地址中的单词。 @@ -1865,8 +1859,7 @@ user_mailer.in_person_verified.next_sign_in.without_sp: 接下来请点击按钮 user_mailer.in_person_verified.sign_in: 登录 user_mailer.in_person_verified.subject: 你在 %{app_name} 成功地验证了身份 user_mailer.in_person_verified.warning_contact_us_html: 如果你没有试图亲身验证身份,请登入 重设密码。要报告这件事,联系 %{app_name} 支持 %{app_name}。 -user_mailer.letter_reminder_14_days.body_html: "%{date_letter_was_se\ - nt} 日你要求了带有验证码的信。
登录%{app_name} 并输入验证码来完成验证你的身份。 %{help_link}.
" +user_mailer.letter_reminder_14_days.body_html: '%{date_letter_was_sent} 日你要求了带有验证码的信。
登录%{app_name} 并输入验证码来完成验证你的身份。 %{help_link}.
' user_mailer.letter_reminder_14_days.did_not_get_a_letter_html: 如果你没有收到这封信, %{another_letter_link_html}。 user_mailer.letter_reminder_14_days.finish: 完成验证你的身份 user_mailer.letter_reminder_14_days.sign_in_and_request_another_letter: 登录要求再发一封信 diff --git a/config/routes.rb b/config/routes.rb index cd0391f6206..8ba42da6396 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,11 +53,6 @@ post '/api/verify/images' => 'idv/image_uploads#create' post '/api/logger' => 'frontend_log#create' - get '/openid_connect/authorize' => 'openid_connect/authorization#index' - get '/openid_connect/logout' => 'openid_connect/logout#show' - post '/openid_connect/logout' => 'openid_connect/logout#create' - delete '/openid_connect/logout' => 'openid_connect/logout#delete' - get '/robots.txt' => 'robots#index' get '/no_js/detect.css' => 'no_js#index', as: :no_js_detect_css @@ -142,8 +137,6 @@ get 'login/add_piv_cac/prompt' => 'users/piv_cac_setup_from_sign_in#prompt' post 'login/add_piv_cac/prompt' => 'users/piv_cac_setup_from_sign_in#decline' - get 'login/add_piv_cac/success' => 'users/piv_cac_setup_from_sign_in#success' - post 'login/add_piv_cac/success' => 'users/piv_cac_setup_from_sign_in#next' get 'login/piv_cac_recommended' => 'users/piv_cac_recommended#show' post 'login/piv_cac_recommended/add' => 'users/piv_cac_recommended#confirm' post 'login/piv_cac_recommended/skip' => 'users/piv_cac_recommended#skip' @@ -275,6 +268,11 @@ get '/account/personal_key' => 'accounts/personal_keys#new', as: :create_new_personal_key post '/account/personal_key' => 'accounts/personal_keys#create' + get '/openid_connect/authorize' => 'openid_connect/authorization#index' + get '/openid_connect/logout' => 'openid_connect/logout#show' + post '/openid_connect/logout' => 'openid_connect/logout#create' + delete '/openid_connect/logout' => 'openid_connect/logout#delete' + get '/otp/send' => 'users/two_factor_authentication#send_code' get '/authentication_methods_setup' => 'users/two_factor_authentication_setup#index' diff --git a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb index bcee2c46ed0..5ff58bb1acd 100644 --- a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb +++ b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb @@ -63,6 +63,15 @@ expect(response).to redirect_to idv_enter_password_path end + + it 'redirects if the user is not allowed to send mail' do + allow(controller.gpo_verify_by_mail_policy).to receive(:send_letter_available?). + and_return(false) + + get :index + + expect(response).to redirect_to idv_enter_password_path + end end describe '#create' do diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index c22af06d3bd..ad235ebe656 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -57,6 +57,7 @@ bad_password_count: 0, sp_request_url_present: false, remember_device: false, + new_device: true, ) end @@ -113,6 +114,24 @@ response end + + it 'tracks as not being from a new device' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', + success: true, + user_id: user.uuid, + user_locked_out: false, + valid_captcha_result: true, + bad_password_count: 0, + sp_request_url_present: false, + remember_device: false, + new_device: false, + ) + end end end @@ -150,7 +169,12 @@ user = create(:user, :fully_registered) stub_analytics - analytics_hash = { + expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original + + post :create, params: { user: { email: user.email.upcase, password: 'invalid_password' } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: false, user_id: user.uuid, user_locked_out: false, @@ -158,19 +182,19 @@ bad_password_count: 1, sp_request_url_present: false, remember_device: false, - } - expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: user.email.upcase, password: 'invalid_password' } } + new_device: nil, + ) expect(subject.session[:sign_in_flow]).to eq(:sign_in) end it 'tracks the authentication attempt for nonexistent user' do stub_analytics - analytics_hash = { + expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original + + post :create, params: { user: { email: 'foo@example.com', password: 'password' } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: false, user_id: 'anonymous-uuid', user_locked_out: false, @@ -178,13 +202,8 @@ bad_password_count: 1, sp_request_url_present: false, remember_device: false, - } - expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: 'foo@example.com', password: 'password' } } + new_device: nil, + ) end it 'tracks unsuccessful authentication for locked out user' do @@ -195,7 +214,11 @@ ) stub_analytics - analytics_hash = { + + post :create, params: { user: { email: user.email.upcase, password: user.password } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: false, user_id: user.uuid, user_locked_out: true, @@ -203,12 +226,8 @@ bad_password_count: 0, sp_request_url_present: false, remember_device: false, - } - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: user.email.upcase, password: user.password } } + new_device: nil, + ) end it 'tracks unsuccessful authentication for failed reCAPTCHA' do @@ -229,6 +248,7 @@ valid_captcha_result: false, bad_password_count: 0, remember_device: false, + new_device: nil, sp_request_url_present: false, ) end @@ -241,7 +261,10 @@ stub_analytics - analytics_hash = { + post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } + post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: false, user_id: user.uuid, user_locked_out: false, @@ -249,18 +272,18 @@ bad_password_count: 2, sp_request_url_present: false, remember_device: false, - } - - post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } + new_device: nil, + ) end it 'tracks the presence of SP request_url in session' do subject.session[:sp] = { request_url: mock_valid_site } stub_analytics - analytics_hash = { + + post :create, params: { user: { email: 'foo@example.com', password: 'password' } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: false, user_id: 'anonymous-uuid', user_locked_out: false, @@ -268,12 +291,8 @@ bad_password_count: 1, sp_request_url_present: true, remember_device: false, - } - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: 'foo@example.com', password: 'password' } } + new_device: nil, + ) end context 'IAL1 user' do @@ -431,7 +450,11 @@ ) stub_analytics - analytics_hash = { + + post :create, params: { user: { email: user.email, password: user.password } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: true, user_id: user.uuid, user_locked_out: false, @@ -439,19 +462,12 @@ bad_password_count: 0, sp_request_url_present: false, remember_device: false, - } - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - profile_encryption_error = { + new_device: true, + ) + expect(@analytics).to have_logged_event( + 'Profile Encryption: Invalid', error: 'Unable to parse encrypted payload', - } - expect(@analytics).to receive(:track_event). - with('Profile Encryption: Invalid', profile_encryption_error) - - post :create, params: { user: { email: user.email, password: user.password } } - + ) expect(controller.user_session[:encrypted_profiles]).to be_nil expect(profile.reload).to_not be_active end @@ -558,7 +574,11 @@ } stub_analytics - analytics_hash = { + + post :create, params: { user: { email: user.email, password: user.password } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: true, user_id: user.uuid, user_locked_out: false, @@ -566,12 +586,8 @@ bad_password_count: 0, sp_request_url_present: false, remember_device: true, - } - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: user.email, password: user.password } } + new_device: true, + ) end end @@ -584,7 +600,11 @@ } stub_analytics - analytics_hash = { + + post :create, params: { user: { email: user.email, password: user.password } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', success: true, user_id: user.uuid, user_locked_out: false, @@ -592,12 +612,8 @@ bad_password_count: 0, sp_request_url_present: false, remember_device: true, - } - - expect(@analytics).to receive(:track_event). - with('Email and Password Authentication', analytics_hash) - - post :create, params: { user: { email: user.email, password: user.password } } + new_device: true, + ) end end diff --git a/spec/features/idv/gpo_disabled_spec.rb b/spec/features/idv/gpo_disabled_spec.rb index 322ed785490..37c90e4f829 100644 --- a/spec/features/idv/gpo_disabled_spec.rb +++ b/spec/features/idv/gpo_disabled_spec.rb @@ -17,7 +17,7 @@ Rails.application.reload_routes! end - it 'allows verification without the option to confirm address with usps', js: true do + it 'allows verification without the option to confirm address with usps', :js do user = user_with_2fa start_idv_from_sp complete_idv_steps_before_phone_step(user) @@ -36,4 +36,37 @@ expect(page).to have_current_path(sign_up_completed_path) end end + + context 'with GPO address verification disallowed for biometric comparison' do + before do + allow(IdentityConfig.store).to receive(:no_verify_by_mail_for_biometric_comparison_enabled). + and_return(true) + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + end + + it 'does not allow verify by mail with biometric comparison', :js do + user = user_with_2fa + start_idv_from_sp(:oidc, biometric_comparison_required: true) + sign_in_and_2fa_user(user) + complete_all_doc_auth_steps(with_selfie: true) + + # Link to the GPO flow should not be visible + expect(page).to_not have_content(t('idv.troubleshooting.options.verify_by_mail')) + + # Directly visiting the verify my mail path does not allow the user to request a letter + visit idv_request_letter_path + expect(page).to have_current_path(idv_phone_path) + end + + it 'does allow verify by mail without biometric comparison', :js do + user = user_with_2fa + start_idv_from_sp(:oidc, biometric_comparison_required: false) + sign_in_and_2fa_user(user) + complete_all_doc_auth_steps(with_selfie: false) + click_on t('idv.troubleshooting.options.verify_by_mail') + + # The user is allowed to visit the request letter path + expect(page).to have_current_path(idv_request_letter_path) + end + end end diff --git a/spec/features/openid_connect/vtr_spec.rb b/spec/features/openid_connect/vtr_spec.rb index 21102cfd3e4..f0abcb9c697 100644 --- a/spec/features/openid_connect/vtr_spec.rb +++ b/spec/features/openid_connect/vtr_spec.rb @@ -88,15 +88,10 @@ expect(page).to have_content(t('two_factor_authentication.two_factor_hspd12_choice_intro')) # User must setup PIV/CAC before continuing - visit setup_piv_cac_path - - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) + select_2fa_option('piv_cac') + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect click_agree_and_continue click_on 'Continue' diff --git a/spec/features/saml/vtr_spec.rb b/spec/features/saml/vtr_spec.rb index 50b1d711eac..0a30581b34f 100644 --- a/spec/features/saml/vtr_spec.rb +++ b/spec/features/saml/vtr_spec.rb @@ -116,16 +116,11 @@ expect(page).to have_content(t('two_factor_authentication.two_factor_hspd12_choice_intro')) # User must setup PIV/CAC before continuing - visit setup_piv_cac_path - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) + select_2fa_option('piv_cac') + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect - click_submit_default click_agree_and_continue click_submit_default expect_successful_saml_redirect diff --git a/spec/features/sign_in/setup_piv_cac_after_sign_in_spec.rb b/spec/features/sign_in/setup_piv_cac_after_sign_in_spec.rb new file mode 100644 index 00000000000..d41afebdc5c --- /dev/null +++ b/spec/features/sign_in/setup_piv_cac_after_sign_in_spec.rb @@ -0,0 +1,82 @@ +require 'rails_helper' + +RSpec.describe 'Setup PIV/CAC after sign-in' do + include SamlAuthHelper + + scenario 'user opts to not add piv/cac card' do + perform_steps_to_get_to_add_piv_cac_during_sign_up + + click_on t('forms.piv_cac_setup.no_thanks') + + expect(page).to have_current_path(sign_up_completed_path) + end + + context 'without an associated service provider' do + scenario 'user opts to not add piv/cac card' do + perform_steps_to_get_to_add_piv_cac_during_sign_up(sp: nil) + + click_on t('forms.piv_cac_setup.no_thanks') + + expect(page).to have_current_path(account_path) + end + end + + scenario 'user opts to add piv/cac card' do + perform_steps_to_get_to_add_piv_cac_during_sign_up + + fill_in t('forms.piv_cac_setup.nickname'), with: 'Card 1' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(sign_up_completed_path) + end + + scenario 'user opts to add piv/cac card but gets an error' do + perform_steps_to_get_to_add_piv_cac_during_sign_up + + fill_in t('forms.piv_cac_setup.nickname'), with: 'Card 1' + stub_piv_cac_service(error: 'certificate.bad') + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(setup_piv_cac_error_path(error: 'certificate.bad')) + end + + scenario 'user opts to add piv/cac card and has piv cac redirect in CSP' do + allow(Identity::Hostdata).to receive(:env).and_return('test') + allow(Identity::Hostdata).to receive(:domain).and_return('example.com') + + perform_steps_to_get_to_add_piv_cac_during_sign_up + + expected_form_action = <<-STR.squish + form-action https://*.pivcac.test.example.com 'self' + http://localhost:7654 https://example.com + STR + + expect(page.response_headers['Content-Security-Policy']). + to(include(expected_form_action)) + end + + def perform_steps_to_get_to_add_piv_cac_during_sign_up(sp: :oidc) + user = create(:user, :fully_registered, :with_phone) + if sp + visit_idp_from_sp_with_ial1(sp) + else + visit new_user_session_path + end + + click_on t('account.login.piv_cac') + stub_piv_cac_service + click_on t('forms.piv_cac_login.submit') + + follow_piv_cac_redirect + expect(page).to have_current_path(login_piv_cac_error_path(error: 'user.not_found')) + click_on t('instructions.mfa.piv_cac.back_to_sign_in') + + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(current_path).to eq login_add_piv_cac_prompt_path + fill_in t('forms.piv_cac_setup.nickname'), with: 'Card 1' + end +end diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index d635017367b..2118db20cb2 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -405,36 +405,24 @@ def attempt_to_bypass_2fa end scenario 'user uses PIV/CAC as their second factor' do - stub_piv_cac_service - user = user_with_piv_cac sign_in_before_2fa(user) + stub_piv_cac_service(uuid: user.piv_cac_configurations.first.x509_dn_uuid) - nonce = visit_login_two_factor_piv_cac_and_get_nonce + click_on t('forms.piv_cac_mfa.submit') + follow_piv_cac_redirect - visit_piv_cac_service( - login_two_factor_piv_cac_path, - uuid: user.piv_cac_configurations.first.x509_dn_uuid, - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', - nonce: nonce, - ) expect(current_path).to eq account_path end scenario 'user uses incorrect PIV/CAC as their second factor' do - stub_piv_cac_service - user = user_with_piv_cac sign_in_before_2fa(user) + stub_piv_cac_service(uuid: Random.uuid) - nonce = visit_login_two_factor_piv_cac_and_get_nonce + click_on t('forms.piv_cac_mfa.submit') + follow_piv_cac_redirect - visit_piv_cac_service( - login_two_factor_piv_cac_path, - uuid: user.piv_cac_configurations.first.x509_dn_uuid + 'X', - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.12345', - nonce: nonce, - ) expect(current_path).to eq login_two_factor_piv_cac_path expect(page).to have_content(t('two_factor_authentication.invalid_piv_cac')) end diff --git a/spec/features/users/piv_cac_management_spec.rb b/spec/features/users/piv_cac_management_spec.rb index 123d268a90d..4951e5709e6 100644 --- a/spec/features/users/piv_cac_management_spec.rb +++ b/spec/features/users/piv_cac_management_spec.rb @@ -9,7 +9,7 @@ allow(Identity::Hostdata).to receive(:env).and_return('test') allow(Identity::Hostdata).to receive(:domain).and_return('example.com') - stub_piv_cac_service + stub_piv_cac_service(uuid:) sign_in_and_2fa_user(user) visit account_two_factor_authentication_path @@ -18,14 +18,9 @@ expect(page.response_headers['Content-Security-Policy'].split(';').map(&:strip)). to(include("form-action https://*.pivcac.test.example.com 'self'")) - nonce = piv_cac_nonce_from_form_action - - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - uuid: uuid, - subject: 'SomeIgnoredSubject', - ) + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect expect(current_path).to eq account_path visit account_two_factor_authentication_path @@ -56,20 +51,15 @@ end scenario 'disallows association of a piv/cac with the same name' do - stub_piv_cac_service + stub_piv_cac_service(uuid:) sign_in_and_2fa_user(user) visit account_two_factor_authentication_path click_link t('account.index.piv_cac_add'), href: setup_piv_cac_url - nonce = piv_cac_nonce_from_form_action - - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - uuid: uuid, - subject: 'SomeIgnoredSubject', - ) + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect expect(current_path).to eq account_path @@ -83,19 +73,16 @@ end scenario 'displays error for piv/cac with no certificate and accepts more error info' do - stub_piv_cac_service + stub_piv_cac_service(error: 'certificate.none') sign_in_and_2fa_user(user) visit account_two_factor_authentication_path click_link t('account.index.piv_cac_add'), href: setup_piv_cac_url - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - error: 'certificate.none', - key_id: 'AB:CD:EF', - ) + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + expect(current_path).to eq setup_piv_cac_error_path expect(page).to have_link(t('instructions.mfa.piv_cac.try_again'), href: setup_piv_cac_url) expect(page).to have_content( @@ -107,19 +94,16 @@ end scenario 'displays error for expires certificate piv/cac and accepts more error info' do - stub_piv_cac_service + stub_piv_cac_service(error: 'certificate.expired') sign_in_and_2fa_user(user) visit account_two_factor_authentication_path click_link t('account.index.piv_cac_add'), href: setup_piv_cac_url - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - error: 'certificate.expired', - key_id: 'AB:CD:EF', - ) + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + expect(current_path).to eq setup_piv_cac_error_path expect(page).to have_link( t('instructions.mfa.piv_cac.please_try_again'), diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 20fceb7bd6b..2fb3daf39cd 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -32,20 +32,6 @@ to have_link t('devise.failure.not_found_in_database_link_text', href: link_url) end - scenario 'user opts to not add piv/cac card' do - perform_steps_to_get_to_add_piv_cac_during_sign_up - click_on t('forms.piv_cac_setup.no_thanks') - expect(current_path).to eq sign_up_completed_path - end - - context 'without an associated service provider' do - scenario 'user opts to not add piv/cac card' do - perform_steps_to_get_to_add_piv_cac_during_sign_up(sp: nil) - click_on t('forms.piv_cac_setup.no_thanks') - expect(current_path).to eq account_path - end - end - scenario 'user is suspended, gets show please call page after 2fa' do user = create(:user, :fully_registered, :suspended) service_provider = ServiceProvider.find_by(issuer: OidcAuthHelper::OIDC_IAL1_ISSUER) @@ -61,22 +47,6 @@ expect(current_path).to eq(user_please_call_path) end - scenario 'user opts to add piv/cac card' do - perform_steps_to_get_to_add_piv_cac_during_sign_up - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - current_url, - nonce: nonce, - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) - - expect(current_path).to eq login_add_piv_cac_success_path - click_continue - expect(current_path).to eq sign_up_completed_path - end - scenario 'user with old terms of use can accept and continue to IAL1 SP' do user = create( :user, @@ -132,36 +102,6 @@ expect(oidc_redirect_url).to start_with service_provider.redirect_uris.first end - scenario 'user opts to add piv/cac card but gets an error' do - perform_steps_to_get_to_add_piv_cac_during_sign_up - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - current_url, - nonce: nonce, - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', - uuid: SecureRandom.uuid, - error: 'certificate.bad', - subject: 'SomeIgnoredSubject', - ) - - expect(page).to have_current_path(login_piv_cac_error_path(error: 'certificate.bad')) - end - - scenario 'user opts to add piv/cac card and has piv cac redirect in CSP' do - allow(Identity::Hostdata).to receive(:env).and_return('test') - allow(Identity::Hostdata).to receive(:domain).and_return('example.com') - - perform_steps_to_get_to_add_piv_cac_during_sign_up - - expected_form_action = <<-STR.squish - form-action https://*.pivcac.test.example.com 'self' - http://localhost:7654 https://example.com - STR - - expect(page.response_headers['Content-Security-Policy']). - to(include(expected_form_action)) - end - scenario 'User with gov/mil email directed to recommended PIV page' do user = create(:user, :with_phone, { email: 'example@example.gov' }) @@ -192,14 +132,8 @@ allow(FeatureManagement).to receive(:development_and_identity_pki_disabled?).and_return(false) stub_piv_cac_service - nonce = get_piv_cac_nonce_from_link(find_link(t('forms.piv_cac_login.submit'))) - visit_piv_cac_service( - current_url, - nonce: nonce, - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect expect(page).to have_current_path(login_piv_cac_error_path(error: 'user.not_found')) visit sign_up_email_path @@ -1095,35 +1029,6 @@ end end - def perform_steps_to_get_to_add_piv_cac_during_sign_up(sp: :oidc) - user = create(:user, :fully_registered, :with_phone) - if sp - visit_idp_from_sp_with_ial1(sp) - else - visit new_user_session_path - end - click_on t('account.login.piv_cac') - allow(FeatureManagement).to receive(:development_and_identity_pki_disabled?).and_return(false) - - stub_piv_cac_service - nonce = get_piv_cac_nonce_from_link(find_link(t('forms.piv_cac_login.submit'))) - visit_piv_cac_service( - current_url, - nonce: nonce, - dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) - - expect(page).to have_current_path(login_piv_cac_error_path(error: 'user.not_found')) - visit new_user_session_path - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - expect(current_path).to eq login_add_piv_cac_prompt_path - fill_in 'name', with: 'Card 1' - end - def with_forgery_protection original_allow_forgery_protection = ActionController::Base.allow_forgery_protection ActionController::Base.allow_forgery_protection = true diff --git a/spec/fixtures/proofing/aamva/requests/verification_request.xml b/spec/fixtures/proofing/aamva/requests/verification_request.xml index 8321a8aba2c..32d5a25010f 100644 --- a/spec/fixtures/proofing/aamva/requests/verification_request.xml +++ b/spec/fixtures/proofing/aamva/requests/verification_request.xml @@ -34,4 +34,4 @@ - + \ No newline at end of file diff --git a/spec/mailers/previews/user_mailer_preview_spec.rb b/spec/mailers/previews/user_mailer_preview_spec.rb index f0ae9d81b88..e82fd5c6e38 100644 --- a/spec/mailers/previews/user_mailer_preview_spec.rb +++ b/spec/mailers/previews/user_mailer_preview_spec.rb @@ -4,8 +4,17 @@ RSpec.describe UserMailerPreview do UserMailerPreview.instance_methods(false).each do |mailer_method| describe "##{mailer_method}" do + subject(:mail) { UserMailerPreview.new.public_send(mailer_method) } + it 'generates a preview without blowing up' do - expect { UserMailerPreview.new.public_send(mailer_method).body }.to_not raise_error + expect { mail.body }.to_not raise_error + end + + it 'does not include any svg images' do + # SVGs are typically the preferred format for their high-quality and small file size, but + # they are not well-supported in email clients. Instead, store a rasterized version of the + # image in `app/assets/images/email` for use in mailer content. + expect(mail.html_part.body).not_to have_selector("img[src$='.svg']") end end end diff --git a/spec/services/gpo_confirmation_uploader_spec.rb b/spec/services/gpo_confirmation_uploader_spec.rb index 8b95c27fb22..0c32972be0e 100644 --- a/spec/services/gpo_confirmation_uploader_spec.rb +++ b/spec/services/gpo_confirmation_uploader_spec.rb @@ -23,8 +23,11 @@ ] end + let(:job_analytics) { FakeAnalytics.new } + before do allow(IdentityConfig.store).to receive(:usps_upload_enabled).and_return(true) + allow(uploader).to receive(:analytics).and_return(job_analytics) end describe '#generate_export' do @@ -111,13 +114,20 @@ log = logs.first expect(log.ftp_at).to be_present expect(log.letter_requests_count).to eq(1) + expect(job_analytics).to have_logged_event( + :gpo_confirmation_upload, + success: true, + exception: nil, + gpo_confirmation_count: confirmations.count, + ) end end context 'when there is an error' do it 'notifies NewRelic and does not clear confirmations if SFTP fails' do expect(uploader).to receive(:generate_export).with(confirmations).and_return(export) - expect(uploader).to receive(:upload_export).with(export).and_raise(StandardError) + expect(uploader).to receive(:upload_export).with(export). + and_raise(StandardError, 'test error') expect(uploader).not_to receive(:clear_confirmations) expect(NewRelic::Agent).to receive(:notice_error) @@ -125,6 +135,12 @@ expect { subject }.to raise_error expect(GpoConfirmation.count).to eq 1 + expect(job_analytics).to have_logged_event( + :gpo_confirmation_upload, + success: false, + exception: 'test error', + gpo_confirmation_count: 0, + ) end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index d8dc562604b..809264ecff6 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -64,21 +64,14 @@ def signin_with_piv(user = user_with_piv_cac) end def signin_with_piv_error(error) - user = user_with_piv_cac visit new_user_session_path click_on t('account.login.piv_cac') allow(FeatureManagement).to receive(:development_and_identity_pki_disabled?).and_return(false) - stub_piv_cac_service - nonce = get_piv_cac_nonce_from_link(find_link(t('forms.piv_cac_login.submit'))) - visit_piv_cac_service( - current_url, - nonce: nonce, - uuid: user.piv_cac_configurations.first.x509_dn_uuid, - subject: 'SomeIgnoredSubject', - error: error, - ) + stub_piv_cac_service(error:) + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect end def signin_with_bad_piv @@ -93,14 +86,9 @@ def fill_in_piv_cac_credentials_and_submit(user, piv_cac_configurations&.first&.x509_dn_uuid) allow(FeatureManagement).to receive(:development_and_identity_pki_disabled?).and_return(false) - stub_piv_cac_service - nonce = get_piv_cac_nonce_from_link(find_link(t('forms.piv_cac_login.submit'))) - visit_piv_cac_service( - current_url, - nonce: nonce, - uuid: uuid, - subject: 'SomeIgnoredSubject', - ) + stub_piv_cac_service(uuid:) + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect end def fill_in_bad_piv_cac_credentials_and_submit @@ -537,16 +525,9 @@ def register_user_with_piv_cac(email = 'test@test.com') def set_up_2fa_with_piv_cac stub_piv_cac_service select_2fa_option('piv_cac') - - expect(page).to have_current_path setup_piv_cac_path - - nonce = piv_cac_nonce_from_form_action - visit_piv_cac_service( - setup_piv_cac_url, - nonce: nonce, - uuid: SecureRandom.uuid, - subject: 'SomeIgnoredSubject', - ) + fill_in t('instructions.mfa.piv_cac.step_1'), with: 'Card' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect end def skip_second_mfa_prompt @@ -559,7 +540,7 @@ def sign_in_via_branded_page(user) click_submit_default end - def stub_piv_cac_service + def stub_piv_cac_service(error: nil, uuid: Random.uuid) allow(IdentityConfig.store).to receive(:identity_pki_disabled).and_return(false) allow(IdentityConfig.store).to receive(:piv_cac_service_url). and_return('http://piv.example.com/') @@ -570,36 +551,34 @@ def stub_piv_cac_service body: CGI.unescape(request.body.sub(/^token=/, '')), } end - end - - def visit_piv_cac_service(idp_url, token_data) - visit(idp_url + '?token=' + CGI.escape(token_data.to_json)) - end - def visit_login_two_factor_piv_cac_and_get_nonce - visit login_two_factor_piv_cac_path - get_piv_cac_nonce_from_link(find_link(t('forms.piv_cac_mfa.submit'))) - end - - # This is a bit convoluted because we generate a nonce when we visit the - # link. The link provides a redirect to the piv/cac service with the nonce. - # This way, even if JavaScript fetches the link to grab the nonce, a new nonce - # is generated when the user clicks on the link. - def get_piv_cac_nonce_from_link(link) - go_back = current_path - visit link['href'] - nonce = Rack::Utils.parse_nested_query(URI(current_url).query)['nonce'] - visit go_back - nonce + stub_request(:post, 'piv.example.com'). + with(query: hash_including('nonce', 'redirect_uri')). + to_return do |request| + query = UriService.params(request.uri) + { + status: 302, + headers: { + location: UriService.add_params( + query['redirect_uri'], + token: { + dn: 'C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234', + uuid:, + subject: 'SomeIgnoredSubject', + nonce: query['nonce'], + error:, + }.compact.to_json, + ), + }, + } + end end - def piv_cac_nonce_from_form_action - go_back = current_path - fill_in 'name', with: 'Card ' + SecureRandom.uuid - click_button t('forms.piv_cac_setup.submit') - nonce = Rack::Utils.parse_nested_query(URI(current_url).query)['nonce'] - visit go_back - nonce + def follow_piv_cac_redirect + # RackTest won't do an external redirect to the stubbed PKI service, but we can manually + # submit a request to the `current_url` and get the redirect header. + redirect_url = Faraday.post(current_url).headers['location'] + visit redirect_url end def link_identity(user, service_provider, ial = nil)