diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0933f040a98..bf70f35a09b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -230,6 +230,7 @@ def after_sign_in_path_for(_user) return authentication_methods_setup_url if user_needs_sp_auth_method_setup? return fix_broken_personal_key_url if current_user.broken_personal_key? return user_session.delete(:stored_location) if user_session.key?(:stored_location) + return setup_piv_cac_url if user_session[:add_piv_cac_after_2fa] return login_add_piv_cac_prompt_url if session[:needs_to_setup_piv_cac_after_sign_in].present? return reactivate_account_url if user_needs_to_reactivate_account? return login_piv_cac_recommended_path if user_recommended_for_piv_cac? diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index e77735ed3f7..9a3502ae4ef 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -154,8 +154,6 @@ def invalid_otp_error(type) t('two_factor_authentication.invalid_otp') when 'personal_key' t('two_factor_authentication.invalid_personal_key') - when 'piv_cac' - t('two_factor_authentication.invalid_piv_cac') else raise "Unsupported otp method: #{type}" end diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb index fec6acb3a8d..9d74854fad7 100644 --- a/app/controllers/two_factor_authentication/options_controller.rb +++ b/app/controllers/two_factor_authentication/options_controller.rb @@ -54,6 +54,7 @@ def two_factor_options_presenter service_provider: current_sp, phishing_resistant_required: service_provider_mfa_policy.phishing_resistant_required?, piv_cac_required: service_provider_mfa_policy.piv_cac_required?, + add_piv_cac_after_2fa: user_session[:add_piv_cac_after_2fa].present?, ) end diff --git a/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb b/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb new file mode 100644 index 00000000000..a6fe0a3bae8 --- /dev/null +++ b/app/controllers/two_factor_authentication/piv_cac_mismatch_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module TwoFactorAuthentication + class PivCacMismatchController < ApplicationController + include TwoFactorAuthenticatable + + def show + analytics.piv_cac_mismatch_visited( + piv_cac_required: piv_cac_required?, + has_other_authentication_methods: has_other_authentication_methods?, + ) + + @piv_cac_required = piv_cac_required? + @has_other_authentication_methods = has_other_authentication_methods? + end + + def create + analytics.piv_cac_mismatch_submitted(add_piv_cac_after_2fa: add_piv_cac_after_2fa?) + user_session[:add_piv_cac_after_2fa] = add_piv_cac_after_2fa? + redirect_to login_two_factor_options_url + end + + private + + def add_piv_cac_after_2fa? + params[:add_piv_cac_after_2fa] == 'true' + end + + def piv_cac_required? + service_provider_mfa_policy.piv_cac_required? + end + + def has_other_authentication_methods? + return @has_other_authentication_methods if defined?(@has_other_authentication_methods) + @has_other_authentication_methods = mfa_context.two_factor_configurations.any? do |config| + config.mfa_enabled? && !config.is_a?(PivCacConfiguration) + end + end + + def mfa_context + @mfa_context ||= MfaContext.new(current_user) + end + end +end diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb index 27ec7494354..a1fb1a0f5ed 100644 --- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb +++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb @@ -58,16 +58,22 @@ def handle_valid_piv_cac def handle_invalid_piv_cac clear_piv_cac_information - handle_invalid_otp(type: 'piv_cac') + update_invalid_user + + if current_user.locked_out? + handle_second_factor_locked_user(type: 'piv_cac') + elsif redirect_for_piv_cac_mismatch_replacement? + redirect_to login_two_factor_piv_cac_mismatch_url + else + flash[:error] = t('two_factor_authentication.invalid_piv_cac') + redirect_to login_two_factor_piv_cac_url + end end - # This overrides the method in TwoFactorAuthenticatable so that we - # redirect back to ourselves rather than rendering the :show template. - # This removes the token from the address bar and preserves the error - # in the flash. - def render_show_after_invalid - flash[:error] = flash.now[:error] - redirect_to login_two_factor_piv_cac_url + def redirect_for_piv_cac_mismatch_replacement? + piv_cac_verification_form.error_type == 'user.piv_cac_mismatch' && + UserSessionContext.authentication_context?(context) && + current_user.piv_cac_configurations.count < IdentityConfig.store.max_piv_cac_per_account end def piv_cac_view_data diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index bea1a258efe..b52d1e3bad9 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -18,6 +18,8 @@ class PivCacAuthenticationSetupController < ApplicationController helper_method :in_multi_mfa_selection_flow? def new + @piv_cac_required = service_provider_mfa_policy.piv_cac_required? + if params.key?(:token) process_piv_cac_setup else @@ -35,7 +37,10 @@ def error end def submit_new_piv_cac - if good_nickname + if skip? + user_session.delete(:add_piv_cac_after_2fa) + redirect_to after_sign_in_path_for(current_user) + elsif good_nickname? user_session[:piv_cac_nickname] = params[:name] create_piv_cac_nonce redirect_to piv_cac_service_url_with_redirect, allow_other_host: true @@ -100,6 +105,7 @@ def process_valid_submission ) create_user_event(:piv_cac_enabled) track_mfa_method_added + user_session.delete(:add_piv_cac_after_2fa) session[:needs_to_setup_piv_cac_after_sign_in] = false redirect_to next_setup_path || after_sign_in_path_for(current_user) end @@ -119,7 +125,11 @@ def process_invalid_submission end end - def good_nickname + def skip? + params[:skip] == 'true' + end + + def good_nickname? name = params[:name] name.present? && !PivCacConfiguration.exists?(user_id: current_user.id, name: name) end diff --git a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb index cb66efbcdff..981415b8e80 100644 --- a/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter.rb @@ -6,6 +6,11 @@ def type :piv_cac end + def render_in(view_context, &block) + @disabled = view_context.user_session.key?(:add_piv_cac_after_2fa) + view_context.capture(&block) + end + def label t('two_factor_authentication.login_options.piv_cac') end @@ -13,5 +18,9 @@ def label def info t('two_factor_authentication.login_options.piv_cac_info') end + + def disabled? + @disabled.present? + end end end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index 3a9488f22dc..bcc13ba0747 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -4,11 +4,16 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresent include AccountResetConcern include ActionView::Helpers::TranslationHelper - attr_reader :user, :reauthentication_context, :phishing_resistant_required, :piv_cac_required + attr_reader :user, + :reauthentication_context, + :phishing_resistant_required, + :piv_cac_required, + :add_piv_cac_after_2fa alias_method :reauthentication_context?, :reauthentication_context alias_method :phishing_resistant_required?, :phishing_resistant_required alias_method :piv_cac_required?, :piv_cac_required + alias_method :add_piv_cac_after_2fa?, :add_piv_cac_after_2fa def initialize( user:, @@ -16,7 +21,8 @@ def initialize( reauthentication_context:, service_provider:, phishing_resistant_required:, - piv_cac_required: + piv_cac_required:, + add_piv_cac_after_2fa: ) @user = user @view = view @@ -24,6 +30,7 @@ def initialize( @service_provider = service_provider @phishing_resistant_required = phishing_resistant_required @piv_cac_required = piv_cac_required + @add_piv_cac_after_2fa = add_piv_cac_after_2fa end def title @@ -47,7 +54,7 @@ def info end def restricted_options_warning_text - return if reauthentication_context? + return if show_all_options? if piv_cac_required? t('two_factor_authentication.aal2_request.piv_cac_only_html', sp_name:) @@ -60,9 +67,9 @@ def options return @options if defined?(@options) mfa = MfaContext.new(user) - if piv_cac_required? && !reauthentication_context? + if piv_cac_required? && !show_all_options? configurations = mfa.piv_cac_configurations - elsif phishing_resistant_required? && !reauthentication_context? + elsif phishing_resistant_required? && !show_all_options? configurations = mfa.phishing_resistant_configurations else configurations = mfa.two_factor_configurations @@ -101,6 +108,10 @@ def first_enabled_option_index private + def show_all_options? + reauthentication_context? || add_piv_cac_after_2fa? + end + def account_reset_link t( 'two_factor_authentication.account_reset.text_html', diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 21596247d4e..c0f90702f37 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -5939,6 +5939,24 @@ def piv_cac_login_visited track_event(:piv_cac_login_visited) end + # User submits prompt to replace PIV/CAC after failing to authenticate due to mismatched subject + # @param [Boolean] add_piv_cac_after_2fa User chooses to replace PIV/CAC authenticator + def piv_cac_mismatch_submitted(add_piv_cac_after_2fa:, **extra) + track_event(:piv_cac_mismatch_submitted, add_piv_cac_after_2fa:, **extra) + end + + # User visits prompt to replace PIV/CAC after failing to authenticate due to mismatched subject + # @param [Boolean] piv_cac_required Partner requires HSPD12 authentication + # @param [Boolean] has_other_authentication_methods User has non-PIV authentication methods + def piv_cac_mismatch_visited(piv_cac_required:, has_other_authentication_methods:, **extra) + track_event( + :piv_cac_mismatch_visited, + piv_cac_required:, + has_other_authentication_methods:, + **extra, + ) + end + # @param [String] action what action user made # Tracks when user submits an action on Piv Cac recommended page def piv_cac_recommended(action: nil, **extra) diff --git a/app/views/two_factor_authentication/options/index.html.erb b/app/views/two_factor_authentication/options/index.html.erb index d17c9914614..9a55980a0ee 100644 --- a/app/views/two_factor_authentication/options/index.html.erb +++ b/app/views/two_factor_authentication/options/index.html.erb @@ -2,6 +2,13 @@ <%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice])) %> +<% if @presenter.add_piv_cac_after_2fa? %> + <%= render AlertComponent.new( + type: :info, + class: 'margin-bottom-4', + ).with_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add')) %> +<% end %> + <%= render PageHeadingComponent.new.with_content(@presenter.heading) %>

diff --git a/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb b/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb new file mode 100644 index 00000000000..eee6ddd11f6 --- /dev/null +++ b/app/views/two_factor_authentication/piv_cac_mismatch/show.html.erb @@ -0,0 +1,38 @@ +<% self.title = t('two_factor_authentication.piv_cac_mismatch.title') %> + +<%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.piv_cac_mismatch.title')) %> + +<% if @has_other_authentication_methods %> +

<%= t('two_factor_authentication.piv_cac_mismatch.instructions') %>

+ + <%= render ButtonComponent.new( + url: login_two_factor_piv_cac_mismatch_url, + method: :post, + params: { add_piv_cac_after_2fa: 'true' }, + big: true, + wide: true, + class: 'display-block margin-top-5', + ).with_content(t('two_factor_authentication.piv_cac_mismatch.cta')) %> + + <% if !@piv_cac_required %> + <%= render ButtonComponent.new( + url: login_two_factor_piv_cac_mismatch_url, + method: :post, + unstyled: true, + class: 'display-block margin-top-2', + ).with_content(t('two_factor_authentication.piv_cac_mismatch.skip')) %> + <% end %> +<% else %> +

<%= t('two_factor_authentication.piv_cac_mismatch.instructions_no_other_method', app_name: APP_NAME) %>

+ + <%= render ButtonComponent.new( + url: account_reset_recovery_options_url, + big: true, + wide: true, + class: 'display-inline-block margin-top-3', + ).with_content(t('two_factor_authentication.piv_cac_mismatch.delete_account')) %> +<% end %> + +<%= render PageFooterComponent.new do %> + <%= link_to t('links.cancel'), sign_out_url %> +<% end %> diff --git a/app/views/users/piv_cac_authentication_setup/new.html.erb b/app/views/users/piv_cac_authentication_setup/new.html.erb index 172c77960dd..1807862381e 100644 --- a/app/views/users/piv_cac_authentication_setup/new.html.erb +++ b/app/views/users/piv_cac_authentication_setup/new.html.erb @@ -31,8 +31,22 @@ <% end %> <% end %> - <%= f.submit t('forms.piv_cac_setup.submit'), class: 'display-block margin-y-5' %> + <%= f.submit t('forms.piv_cac_setup.submit'), class: 'display-block margin-top-5 margin-bottom-2' %> <% end %> +<% if user_session[:add_piv_cac_after_2fa] && !@piv_cac_required %> + <%= render ButtonComponent.new( + url: submit_new_piv_cac_url, + method: :post, + params: { skip: 'true' }, + unstyled: true, + ).with_content(t('mfa.skip')) %> +<% end %> -<%= render 'shared/cancel_or_back_to_options' %> +<% if user_session[:add_piv_cac_after_2fa] %> + <%= render PageFooterComponent.new do %> + <%= link_to t('links.cancel'), sign_out_path %> + <% end %> +<% else %> + <%= render 'shared/cancel_or_back_to_options' %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ebd7685fc38..c4e56d5d3c2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1716,6 +1716,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received: two_factor_authentication.phone.delete.failure: Unable to remove your phone. two_factor_authentication.phone.delete.success: Your phone has been removed. two_factor_authentication.piv_cac_header_text: Insert your government employee ID +two_factor_authentication.piv_cac_mismatch.2fa_before_add: You need to authenticate with another method before adding your PIV/CAC. +two_factor_authentication.piv_cac_mismatch.cta: Authenticate and add PIV/CAC +two_factor_authentication.piv_cac_mismatch.delete_account: Delete your account +two_factor_authentication.piv_cac_mismatch.instructions: Click “Authenticate and add PIV/CAC” below to authenticate with another method before adding this PIV/CAC to your account. +two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: If you were reissued your PIV/CAC, you will need to delete your %{app_name} account and create a new account to use your reissued PIV/CAC. +two_factor_authentication.piv_cac_mismatch.skip: Skip adding PIV/CAC +two_factor_authentication.piv_cac_mismatch.title: This government employee ID is not connected to your account two_factor_authentication.piv_cac_upsell.add_piv: Add PIV/CAC card two_factor_authentication.piv_cac_upsell.choose_other_method: Choose other methods instead two_factor_authentication.piv_cac_upsell.explain: This will improve your account security and let you skip entering your email and password when signing in. diff --git a/config/locales/es.yml b/config/locales/es.yml index 3497461ffe1..5ab154e9ebd 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1728,6 +1728,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received: two_factor_authentication.phone.delete.failure: No se puede eliminar su teléfono. two_factor_authentication.phone.delete.success: Su teléfono fue eliminado. two_factor_authentication.piv_cac_header_text: Inserte su identificación de empleado del gobierno +two_factor_authentication.piv_cac_mismatch.2fa_before_add: Debe realizar la autenticación con otro método antes de añadir su tarjeta PIV o CAC. +two_factor_authentication.piv_cac_mismatch.cta: Autenticar y añadir tarjeta PIV o CAC +two_factor_authentication.piv_cac_mismatch.delete_account: Eliminar su cuenta +two_factor_authentication.piv_cac_mismatch.instructions: Haga clic en “Autenticar y añadir tarjeta PIV o CAC” más abajo para autenticar con otro método antes de añadir esta tarjeta PIV o CAC a su cuenta. +two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: Si se le emitió una nueva tarjeta PIV o CAC, para poder usarla, deberá eliminar su cuenta de %{app_name} y crear una cuenta nueva. +two_factor_authentication.piv_cac_mismatch.skip: Saltar añadir tarjeta PIV o CAC +two_factor_authentication.piv_cac_mismatch.title: Esta tarjeta de identificación de empleado del gobierno no está conectada a su cuenta two_factor_authentication.piv_cac_upsell.add_piv: Agregar tarjeta PIV/CAC two_factor_authentication.piv_cac_upsell.choose_other_method: Elegir otros métodos two_factor_authentication.piv_cac_upsell.explain: Esto hará que su cuenta sea más segura y no tendrá que ingresar su correo electrónico ni su contraseña cuando inicie sesión. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e49b209a072..1c7fd9cb6e1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1716,6 +1716,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received: two_factor_authentication.phone.delete.failure: Impossible de supprimer votre téléphone. two_factor_authentication.phone.delete.success: Votre téléphone a été supprimé. two_factor_authentication.piv_cac_header_text: Insérer votre carte d’employé fédéral +two_factor_authentication.piv_cac_mismatch.2fa_before_add: Vous devez vous authentifier à l’aide d’une autre méthode avant d’ajouter votre carte PIV/CAC. +two_factor_authentication.piv_cac_mismatch.cta: S’authentifier et ajouter une carte PIV/CAC +two_factor_authentication.piv_cac_mismatch.delete_account: Supprimer votre compte +two_factor_authentication.piv_cac_mismatch.instructions: Cliquez sur « S’authentifier et ajouter une carte PIV/CAC » ci-dessous pour vous authentifier au moyen d’une autre méthode avant d’ajouter cette carte PIV/CAC à votre compte. +two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: Si l’on vous a à nouveau délivré une carte PIV/CAC, vous devez supprimer votre compte %{app_name} et en créer un nouveau pour l’utiliser. +two_factor_authentication.piv_cac_mismatch.skip: Sauter l’ajout de carte PIV/CAC +two_factor_authentication.piv_cac_mismatch.title: Cette carte d’employé fédéral n’est pas associée à votre compte. two_factor_authentication.piv_cac_upsell.add_piv: Ajouter une carte PIV/CAC two_factor_authentication.piv_cac_upsell.choose_other_method: Choisir plutôt d’autres méthodes two_factor_authentication.piv_cac_upsell.explain: Ceci permettra de renforcer la sécurité de votre compte et de sauter l’étape de saisie de votre e-mail et mot de passe quand vous vous connecterez. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index bcc3a9451e0..d7e911f2dc5 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1729,6 +1729,13 @@ two_factor_authentication.phone_verification.troubleshooting.code_not_received: two_factor_authentication.phone.delete.failure: 无法去掉你的电话。 two_factor_authentication.phone.delete.success: 你的电话已被去掉。 two_factor_authentication.piv_cac_header_text: 插入您的政府雇员ID +two_factor_authentication.piv_cac_mismatch.2fa_before_add: 添加你的PIV/CAC之前,你需要使用另外一种方法进行身份证实。 +two_factor_authentication.piv_cac_mismatch.cta: 进行身份证实并添加PIV/CAC +two_factor_authentication.piv_cac_mismatch.delete_account: 删除你的帐户 +two_factor_authentication.piv_cac_mismatch.instructions: 点击下边的“进行身份证实并添加PIV/CAC”,以在将该PIV/CAC添加到你账户之前使用另外一种方法进行身份证实。 +two_factor_authentication.piv_cac_mismatch.instructions_no_other_method: 如果你的PIV/CAV是重新颁发的,你需要删除自己的%{app_name}帐户,并使用重新颁发的PIV/CAC 设立一个新账户。 +two_factor_authentication.piv_cac_mismatch.skip: 跳过添加 PIV/CAC +two_factor_authentication.piv_cac_mismatch.title: 该政府雇员身份证件与你的账户没有连接起来 two_factor_authentication.piv_cac_upsell.add_piv: 添加 PIV/CAC 卡 two_factor_authentication.piv_cac_upsell.choose_other_method: 选择其他方法 two_factor_authentication.piv_cac_upsell.explain: 这将改善你账户安全,而且你在登录时无需再输入电邮和密码。 diff --git a/config/routes.rb b/config/routes.rb index 6952004af90..0a903eab6c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -119,6 +119,9 @@ get '/login/two_factor/options' => 'two_factor_authentication/options#index' post '/login/two_factor/options' => 'two_factor_authentication/options#create' + get '/login/two_factor/piv_cac_mismatch' => 'two_factor_authentication/piv_cac_mismatch#show' + post '/login/two_factor/piv_cac_mismatch' => 'two_factor_authentication/piv_cac_mismatch#create' + get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show' post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create' get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show' diff --git a/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb new file mode 100644 index 00000000000..fdf1d2caef8 --- /dev/null +++ b/spec/controllers/two_factor_authentication/piv_cac_mismatch_controller_spec.rb @@ -0,0 +1,178 @@ +require 'rails_helper' + +RSpec.describe TwoFactorAuthentication::PivCacMismatchController do + let(:user) { create(:user, :with_piv_or_cac) } + + before do + stub_sign_in_before_2fa(user) if user + end + + describe '#show' do + subject(:response) { get :show } + + context 'with user having piv as their only authentication method' do + let(:user) { create(:user, :with_piv_or_cac) } + + it 'assigns has_other_authentication_methods as false' do + response + + expect(assigns(:has_other_authentication_methods)).to eq(false) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_visited, + piv_cac_required: false, + has_other_authentication_methods: false, + ) + end + end + + context 'with user having other authentication methods' do + let(:user) { create(:user, :with_piv_or_cac, :with_phone) } + + it 'assigns has_other_authentication_methods as true' do + response + + expect(assigns(:has_other_authentication_methods)).to eq(true) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_visited, + piv_cac_required: false, + has_other_authentication_methods: true, + ) + end + end + + context 'with partner not requiring hspd12 authentication' do + before do + controller.session[:sp] = { + issuer: SamlAuthHelper::SP_ISSUER, + acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + } + end + + it 'assigns piv_cac_required as false' do + response + + expect(assigns(:piv_cac_required)).to eq(false) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_visited, + piv_cac_required: false, + has_other_authentication_methods: false, + ) + end + end + + context 'with partner requiring hspd12 authentication' do + before do + controller.session[:sp] = { + issuer: SamlAuthHelper::SP_ISSUER, + acr_values: Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF, + } + end + + it 'assigns piv_cac_required as true' do + response + + expect(assigns(:piv_cac_required)).to eq(true) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_visited, + piv_cac_required: true, + has_other_authentication_methods: false, + ) + end + end + + context 'if user is not signed in' do + let(:user) { nil } + + it 'redirects user to sign in' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'if user is already authenticated' do + let(:user) { nil } + + before do + stub_sign_in + end + + it 'redirects user to the signed in path' do + expect(response).to redirect_to(account_path) + end + end + end + + describe '#create' do + let(:params) { {} } + subject(:response) { post :create, params: params } + + context 'when user chooses to add piv' do + let(:params) { { add_piv_cac_after_2fa: 'true' } } + + it 'assigns session value to add piv after authenticating' do + response + + expect(controller.user_session[:add_piv_cac_after_2fa]).to eq(true) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_submitted, + add_piv_cac_after_2fa: true, + ) + end + end + + context 'when user chooses to skip adding piv' do + let(:params) { {} } + + it 'assigns session value to skip adding piv after authenticating' do + response + + expect(controller.user_session[:add_piv_cac_after_2fa]).to eq(false) + end + + it 'logs an analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :piv_cac_mismatch_submitted, + add_piv_cac_after_2fa: false, + ) + end + end + end +end diff --git a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb index c0b012271a6..70dab21a99a 100644 --- a/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/piv_cac_verification_controller_spec.rb @@ -34,11 +34,7 @@ 'key_id' => 'foo', ) allow(PivCacService).to receive(:decode_token).with('bad-token').and_return( - 'uuid' => 'bad-uuid', - 'subject' => bad_dn, - 'issuer' => x509_issuer, - 'nonce' => nonce, - 'key_id' => 'foo', + 'error' => 'token.bad', ) allow(PivCacService).to receive(:decode_token).with('bad-nonce').and_return( 'uuid' => user.piv_cac_configurations.first.x509_dn_uuid, @@ -206,26 +202,50 @@ end context 'when the user presents a different piv/cac' do + subject(:response) { get :show, params: { token: 'good-other-token' } } + before do stub_sign_in_before_2fa(user) - - get :show, params: { token: 'good-other-token' } end it 'increments second_factor_attempts_count' do - expect(subject.current_user.reload.second_factor_attempts_count).to eq 1 - end + response - it 'redirects to the piv/cac entry screen' do - expect(response).to redirect_to login_two_factor_piv_cac_path + expect(controller.current_user.reload.second_factor_attempts_count).to eq 1 end - it 'displays flash error message' do - expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac') + it 'redirects to the piv/cac mismatch screen' do + expect(response).to redirect_to login_two_factor_piv_cac_mismatch_path end it 'resets the piv/cac session information' do - expect(subject.user_session[:decrypted_x509]).to be_nil + response + + expect(controller.user_session[:decrypted_x509]).to be_nil + end + + context 'when user session context is not authentication' do + before do + allow(UserSessionContext).to receive(:authentication_context?).and_return(false) + end + + it 'redirects to authenticate again, including error message' do + expect(response).to redirect_to login_two_factor_piv_cac_path + expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac') + end + end + + context 'when user has maximum number of piv/cac associated with their account' do + before do + while user.piv_cac_configurations.count < IdentityConfig.store.max_piv_cac_per_account + create(:piv_cac_configuration, user:) + end + end + + it 'redirects to authenticate again, including error message' do + expect(response).to redirect_to login_two_factor_piv_cac_path + expect(flash[:error]).to eq t('two_factor_authentication.invalid_piv_cac') + end end end @@ -240,8 +260,6 @@ stub_analytics - piv_cac_mismatch = { type: 'user.piv_cac_mismatch' } - expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::MfaLimitAccountLockedEvent.new(user: subject.current_user)) @@ -251,13 +269,11 @@ expect(@analytics).to have_logged_event( 'Multi-Factor Authentication', success: false, - errors: piv_cac_mismatch, + errors: { type: 'token.invalid' }, context: 'authentication', multi_factor_auth_method: 'piv_cac', enabled_mfa_methods_count: 2, new_device: true, - key_id: 'foo', - piv_cac_configuration_dn_uuid: 'bad-uuid', attempts: 1, ) expect(@analytics).to have_logged_event('Multi-Factor Authentication: max attempts reached') diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb index 46eb1293c6c..1d1e42bcef7 100644 --- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb @@ -13,18 +13,11 @@ end describe '#new' do - context 'when not signed in' do - it 'redirects to root url' do - get :new - - expect(response).to redirect_to(root_url) - end - end + let(:params) { nil } + subject(:response) { get :new, params: params } context 'when signed out' do it 'redirects to sign in page' do - get :new - expect(response).to redirect_to(new_user_session_url) end end @@ -37,15 +30,37 @@ end it 'redirects to 2fa entry' do - get :new - expect(response).to redirect_to(user_two_factor_authentication_url) end end context 'when signed in' do + let(:user) { create(:user, :fully_registered) } before { stub_sign_in(user) } + it 'assigns piv_cac_required instance variable as false' do + response + + expect(assigns(:piv_cac_required)).to eq(false) + end + + context 'when SP requires PIV/CAC' do + let(:service_provider) { create(:service_provider) } + + before do + controller.session[:sp] = { + issuer: service_provider.issuer, + acr_values: Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF, + } + end + + it 'assigns piv_cac_required instance variable as true' do + response + + expect(assigns(:piv_cac_required)).to eq(true) + end + end + context 'without associated piv/cac' do let(:user) do create(:user, :fully_registered, with: { phone: '+1 (703) 555-0000' }) @@ -55,8 +70,8 @@ before(:each) do allow(PivCacService).to receive(:decode_token).with(good_token) { good_token_response } allow(PivCacService).to receive(:decode_token).with(bad_token) { bad_token_response } - allow(subject).to receive(:user_session).and_return(piv_cac_nonce: nonce) - subject.user_session[:piv_cac_nickname] = nickname + allow(controller).to receive(:user_session).and_return(piv_cac_nonce: nonce) + controller.user_session[:piv_cac_nickname] = nickname end let(:nonce) { 'nonce' } @@ -80,14 +95,13 @@ context 'when rendered without a token' do it 'renders the "new" template' do - get :new expect(response).to render_template(:new) end it 'tracks the analytic event of visited' do stub_analytics - get :new + response expect(@analytics).to have_logged_event( :piv_cac_setup_visited, @@ -98,19 +112,19 @@ end context 'when redirected with a good token' do - let(:user) do - create(:user) - end + let(:params) { { token: good_token } } + let(:user) { create(:user) } let(:mfa_selections) { ['piv_cac', 'voice'] } + before do - subject.user_session[:mfa_selections] = mfa_selections + controller.user_session[:mfa_selections] = mfa_selections end context 'with no additional MFAs chosen on setup' do let(:mfa_selections) { ['piv_cac'] } it 'redirects to suggest 2nd MFA page' do stub_analytics - get :new, params: { token: good_token } + expect(response).to redirect_to(auth_method_confirmation_url) expect(@analytics).to have_logged_event( @@ -126,8 +140,9 @@ it 'logs mfa attempts commensurate to number of attempts' do stub_analytics + get :new, params: { token: bad_token } - get :new, params: { token: good_token } + response expect(@analytics).to have_logged_event( 'Multi-Factor Authentication Setup', @@ -141,59 +156,99 @@ end it 'sets the piv/cac session information' do - get :new, params: { token: good_token } + response + json = { 'subject' => 'some dn', 'issuer' => nil, 'presented' => true, }.to_json - expect(subject.user_session[:decrypted_x509]).to eq json + expect(controller.user_session[:decrypted_x509]).to eq json end it 'sets the session to not require piv setup upon sign-in' do - get :new, params: { token: good_token } + response + + expect(controller.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false + end + + context 'when user adds after piv cac mismatch error' do + before do + controller.user_session[:add_piv_cac_after_2fa] = true + end + + it 'deletes add_piv_cac_after_2fa session value' do + response - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false + expect(controller.user_session).not_to have_key(:add_piv_cac_after_2fa) + end end end context 'with additional MFAs leftover' do it 'redirects to Mfa Confirmation page' do - get :new, params: { token: good_token } expect(response).to redirect_to(phone_setup_url) end it 'sets the piv/cac session information' do - get :new, params: { token: good_token } + response + json = { 'subject' => 'some dn', 'issuer' => nil, 'presented' => true, }.to_json - expect(subject.user_session[:decrypted_x509]).to eq json + expect(controller.user_session[:decrypted_x509]).to eq json end it 'sets the session to not require piv setup upon sign-in' do - get :new, params: { token: good_token } + response - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false + expect(controller.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false end end end context 'when redirected with an error token' do + let(:params) { { token: bad_token } } + it 'renders the error template' do - get :new, params: { token: bad_token } expect(response).to redirect_to setup_piv_cac_error_path(error: 'certificate.bad') end it 'resets the piv/cac session information' do - expect(subject.user_session[:decrypted_x509]).to be_nil + response + + expect(controller.user_session[:decrypted_x509]).to be_nil end end end end end + + describe '#submit_new_piv_cac' do + let(:user) { create(:user, :fully_registered) } + + before { stub_sign_in(user) } + + context 'when user opts to skip adding piv cac after 2fa' do + subject(:response) { post :submit_new_piv_cac, params: { skip: 'true' } } + + before do + allow(controller).to receive(:user_session).and_return(add_piv_cac_after_2fa: true) + end + + it 'deletes add_piv_cac_after_2fa session value' do + response + + expect(controller.user_session).not_to have_key(:add_piv_cac_after_2fa) + end + + it 'redirects to after sign in path' do + expect(response).to redirect_to(account_path) + end + end + end end diff --git a/spec/factories/piv_cac_configurations.rb b/spec/factories/piv_cac_configurations.rb index 68c73f0b22b..2a7cea7ddb0 100644 --- a/spec/factories/piv_cac_configurations.rb +++ b/spec/factories/piv_cac_configurations.rb @@ -3,7 +3,7 @@ factory :piv_cac_configuration do name { Faker::Lorem.unique.words.join(' ') } - x509_dn_uuid { 'helloworld' } + x509_dn_uuid { Random.uuid } user end end diff --git a/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb b/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb new file mode 100644 index 00000000000..6f00e16973a --- /dev/null +++ b/spec/features/two_factor_authentication/piv_cac_sign_in_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' + +RSpec.feature 'sign in with piv/cac' do + include SamlAuthHelper + include OidcAuthHelper + + let(:user) { create(:user, :with_piv_or_cac, :with_phone) } + + before do + sign_in_before_2fa(user) + end + + context 'with piv/cac mismatch error' do + before do + stub_piv_cac_service(error: 'user.piv_cac_mismatch') + end + + it 'allows a user to add a replacement piv after authenticating with another method' do + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path) + expect(page).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + + click_on t('two_factor_authentication.piv_cac_mismatch.cta') + + expect(page).to have_current_path(login_two_factor_options_path) + expect(page).to have_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add')) + expect(page).to have_field(t('two_factor_authentication.login_options.sms')) + expect(page).to have_field( + t('two_factor_authentication.login_options.piv_cac'), + disabled: true, + ) + + select_2fa_option(:sms) + fill_in_code_with_last_phone_otp + click_submit_default + + expect(page).to have_current_path(setup_piv_cac_path) + expect(page).to have_button(t('mfa.skip')) + + stub_piv_cac_service + fill_in t('forms.totp_setup.totp_step_1'), with: 'New PIV' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(account_path) + within(page.find('.card', text: t('headings.account.federal_employee_id'))) do + expect(page).to have_css('lg-manageable-authenticator', count: 2) + end + end + + context 'with partner requiring piv/cac' do + before do + visit_idp_from_oidc_sp_with_hspd12_and_require_piv_cac + end + + it 'does not allow a user to skip adding piv/cac' do + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path) + expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + + click_on t('two_factor_authentication.piv_cac_mismatch.cta') + + expect(page).to have_current_path(login_two_factor_options_path) + expect(page).to have_content(t('two_factor_authentication.piv_cac_mismatch.2fa_before_add')) + expect(page).to have_field(t('two_factor_authentication.login_options.sms')) + expect(page).to have_field( + t('two_factor_authentication.login_options.piv_cac'), + disabled: true, + ) + + select_2fa_option(:sms) + fill_in_code_with_last_phone_otp + click_submit_default + + expect(page).to have_current_path(setup_piv_cac_path) + expect(page).not_to have_button(t('mfa.skip')) + + stub_piv_cac_service + fill_in t('forms.totp_setup.totp_step_1'), with: 'New PIV' + click_on t('forms.piv_cac_setup.submit') + follow_piv_cac_redirect + + expect(current_path).to eq(sign_up_completed_path) + + click_agree_and_continue + expect(oidc_decoded_id_token[:x509_presented]).to eq(true) + expect(oidc_decoded_id_token[:x509_subject]).to be_present + end + end + + context 'if the user chooses to skip adding piv/cac when prompted with mismatch' do + it 'allows the user to authenticate with another method' do + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path) + expect(page).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + + click_on t('two_factor_authentication.piv_cac_mismatch.skip') + + expect(page).to have_current_path(login_two_factor_options_path) + expect(page).not_to have_content( + t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'), + ) + expect(page).to have_field(t('two_factor_authentication.login_options.sms')) + expect(page).to have_field( + t('two_factor_authentication.login_options.piv_cac'), + disabled: true, + ) + + select_2fa_option(:sms) + fill_in_code_with_last_phone_otp + click_submit_default + + expect(page).to have_current_path(account_path) + end + end + + context 'with no other mfa methods available' do + let(:user) { create(:user, :with_piv_or_cac) } + + it 'prompts the user to reset their account' do + click_on t('forms.piv_cac_login.submit') + follow_piv_cac_redirect + + expect(page).to have_current_path(login_two_factor_piv_cac_mismatch_path) + expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.cta')) + expect(page).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + expect(page).to have_link(t('two_factor_authentication.piv_cac_mismatch.delete_account')) + + click_on t('two_factor_authentication.piv_cac_mismatch.delete_account') + + expect(page).to have_current_path(account_reset_recovery_options_path) + end + end + 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 ee411103d0f..06f09bf1949 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -415,18 +415,6 @@ def attempt_to_bypass_2fa expect(current_path).to eq account_path end - scenario 'user uses incorrect PIV/CAC as their second factor' do - user = user_with_piv_cac - sign_in_before_2fa(user) - stub_piv_cac_service(uuid: Random.uuid) - - click_on t('forms.piv_cac_mfa.submit') - follow_piv_cac_redirect - - expect(current_path).to eq login_two_factor_piv_cac_path - expect(page).to have_content(t('two_factor_authentication.invalid_piv_cac')) - end - context 'user with Voice preference sends SMS, causing a Telephony error' do let(:user) do create( diff --git a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb index e2d6d5fa8bb..274044ff3da 100644 --- a/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/sign_in_piv_cac_selection_presenter_spec.rb @@ -14,6 +14,43 @@ end end + describe '#render_in' do + let(:user_session) { {} } + let(:view_context) { ActionController::Base.new.view_context } + + before do + allow(view_context).to receive(:user_session).and_return(user_session) + end + + it 'assigns disabled instance variable to false ahead of capture' do + expect(view_context).to receive(:capture) do + expect(presenter.instance_variable_get(:@disabled)).to eq(false) + end + + presenter.render_in(view_context) + end + + it 'renders captured block content' do + expect(view_context).to receive(:capture) do |*_args, &block| + expect(block.call).to eq('content') + end + + presenter.render_in(view_context) { 'content' } + end + + context 'when view context user session incudes add_piv_cac_after_2fa key' do + let(:user_session) { { add_piv_cac_after_2fa: :anything } } + + it 'assigns disabled instance variable to true ahead of capture' do + expect(view_context).to receive(:capture) do + expect(presenter.instance_variable_get(:@disabled)).to eq(true) + end + + presenter.render_in(view_context) + end + end + end + describe '#label' do it 'returns the label text' do expect(presenter.label).to eq( @@ -29,4 +66,24 @@ ) end end + + describe '#disabled?' do + subject(:disabled?) { presenter.disabled? } + + context 'when disabled instance variable is unassigned' do + it { is_expected.to eq(false) } + end + + context 'when disabled instance variable is false' do + before { presenter.instance_variable_set(:@disabled, false) } + + it { is_expected.to eq(false) } + end + + context 'when disabled instance variable is true' do + before { presenter.instance_variable_set(:@disabled, true) } + + it { is_expected.to eq(true) } + end + end end diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb index 14da580896f..c6f02a6211c 100644 --- a/spec/presenters/two_factor_login_options_presenter_spec.rb +++ b/spec/presenters/two_factor_login_options_presenter_spec.rb @@ -9,15 +9,17 @@ let(:piv_cac_required) { false } let(:reauthentication_context) { false } let(:service_provider) { nil } + let(:add_piv_cac_after_2fa) { false } subject(:presenter) do TwoFactorLoginOptionsPresenter.new( - user: user, - view: view, - reauthentication_context: reauthentication_context, - service_provider: service_provider, - phishing_resistant_required: phishing_resistant_required, - piv_cac_required: piv_cac_required, + user:, + view:, + reauthentication_context:, + service_provider:, + phishing_resistant_required:, + piv_cac_required:, + add_piv_cac_after_2fa:, ) end @@ -47,6 +49,12 @@ it { should eq t('two_factor_authentication.login_intro') } end + context 'add piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it { should eq t('two_factor_authentication.login_intro') } + end + context 'reauthentication user session context' do let(:reauthentication_context) { true } @@ -146,6 +154,24 @@ ) end end + + context 'add piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it 'returns all mfas associated with account' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInWebauthnSelectionPresenter, + TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, + TwoFactorAuthentication::SignInPivCacSelectionPresenter, + TwoFactorAuthentication::SignInAuthAppSelectionPresenter, + TwoFactorAuthentication::SignInPersonalKeySelectionPresenter, + ], + ) + end + end end context 'phishing resistant required' do @@ -177,6 +203,24 @@ ) end end + + context 'add piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it 'returns all mfas associated with account' do + expect(options_classes).to eq( + [ + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInPhoneSelectionPresenter, + TwoFactorAuthentication::SignInWebauthnSelectionPresenter, + TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, + TwoFactorAuthentication::SignInPivCacSelectionPresenter, + TwoFactorAuthentication::SignInAuthAppSelectionPresenter, + TwoFactorAuthentication::SignInPersonalKeySelectionPresenter, + ], + ) + end + end end end @@ -212,6 +256,12 @@ it { should be_nil } end + + context 'add piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it { should be_nil } + end end context 'piv cac required' do @@ -241,6 +291,12 @@ it { should be_nil } end + + context 'add piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it { should be_nil } + end end end diff --git a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb index 46e29eb14d9..97e379c4024 100644 --- a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb @@ -5,18 +5,22 @@ let(:phishing_resistant_required) { false } let(:piv_cac_required) { false } let(:reauthentication_context) { false } + let(:add_piv_cac_after_2fa) { false } + + subject(:rendered) { render } before do allow(view).to receive(:user_session).and_return({}) allow(view).to receive(:current_user).and_return(User.new) @presenter = TwoFactorLoginOptionsPresenter.new( - user: user, - view: view, - reauthentication_context: reauthentication_context, + user:, + view:, + reauthentication_context:, service_provider: nil, - phishing_resistant_required: phishing_resistant_required, - piv_cac_required: piv_cac_required, + phishing_resistant_required:, + piv_cac_required:, + add_piv_cac_after_2fa:, ) @two_factor_options_form = TwoFactorLoginOptionsForm.new(user) end @@ -26,36 +30,34 @@ t('two_factor_authentication.login_options_title'), ) - render + rendered end it 'has a localized heading' do - render - expect(rendered).to have_content \ t('two_factor_authentication.login_options_title') end it 'has a localized intro text' do - render - expect(rendered).to have_content \ t('two_factor_authentication.login_intro') end it 'has a cancel link' do - render - expect(rendered).to have_link(t('links.cancel_account_creation'), href: sign_up_cancel_path) end + it 'does not display info text for adding piv cac after 2fa' do + expect(rendered).not_to have_content( + t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'), + ) + end + context 'phone vendor outage' do before do create(:phone_configuration, user: user, phone: '(202) 555-1111') allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).and_return(false) allow_any_instance_of(OutageStatus).to receive(:vendor_outage?).with(:sms).and_return(true) - - render end it 'renders alert banner' do @@ -76,11 +78,20 @@ end end + context 'when adding piv cac after 2fa' do + let(:add_piv_cac_after_2fa) { true } + + it 'displays info text for adding piv cac after 2fa' do + expect(rendered).to have_selector( + '.usa-alert.usa-alert--info', + text: t('two_factor_authentication.piv_cac_mismatch.2fa_before_add'), + ) + end + end + context 'with phishing resistant required' do let(:phishing_resistant_required) { true } - before { render } - it 'displays warning text' do expect(rendered).to have_selector( '.usa-alert.usa-alert--warning', @@ -94,8 +105,6 @@ context 'with piv cac required' do let(:piv_cac_required) { true } - before { render } - it 'displays warning text' do expect(rendered).to have_selector( '.usa-alert.usa-alert--warning', @@ -110,15 +119,11 @@ let(:reauthentication_context) { true } it 'has a localized heading' do - render - expect(rendered).to have_content \ t('two_factor_authentication.login_options_reauthentication_title') end it 'has a localized intro text' do - render - expect(rendered).to have_content \ t('two_factor_authentication.login_intro_reauthentication') end diff --git a/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb b/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb new file mode 100644 index 00000000000..d89a4e71fc8 --- /dev/null +++ b/spec/views/two_factor_authentication/piv_cac_mismatch/show.html.erb_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe 'two_factor_authentication/piv_cac_mismatch/show.html.erb' do + let(:has_other_authentication_methods) {} + let(:piv_cac_required) {} + + subject(:rendered) { render } + + before do + @has_other_authentication_methods = has_other_authentication_methods + @piv_cac_required = piv_cac_required + allow(view).to receive(:user_session).and_return({}) + end + + context 'when user does not have other authentication methods' do + let(:has_other_authentication_methods) { false } + + it 'renders instructions with a link to delete their account' do + expect(rendered).to have_content( + t( + 'two_factor_authentication.piv_cac_mismatch.instructions_no_other_method', + app_name: APP_NAME, + ), + ) + expect(rendered).to have_link( + t('two_factor_authentication.piv_cac_mismatch.delete_account'), + href: account_reset_recovery_options_url, + ) + end + end + + context 'when user has other authentication methods' do + let(:has_other_authentication_methods) { true } + + it 'renders instructions with a link to authenticate' do + expect(rendered).to have_content(t('two_factor_authentication.piv_cac_mismatch.instructions')) + expect(rendered).to have_button(t('two_factor_authentication.piv_cac_mismatch.cta')) + end + + context 'when piv cac is required' do + let(:piv_cac_required) { true } + + it 'does not provide an option to skip setting up piv/cac' do + expect(rendered).not_to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + end + end + + context 'when piv cac is not required' do + let(:piv_cac_required) { false } + + it 'provides an option to skip setting up piv/cac' do + expect(rendered).to have_button(t('two_factor_authentication.piv_cac_mismatch.skip')) + end + end + end +end diff --git a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb index 470609ab22d..4409370f49f 100644 --- a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb +++ b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb @@ -1,35 +1,77 @@ require 'rails_helper' RSpec.describe 'users/piv_cac_authentication_setup/new.html.erb' do + let(:user) { create(:user) } + let(:user_session) { {} } + let(:in_multi_mfa_selection_flow) { false } + + subject(:rendered) { render } + + before do + allow(view).to receive(:user_session).and_return(user_session) + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(in_multi_mfa_selection_flow) + form = OpenStruct.new + @presenter = PivCacAuthenticationSetupPresenter.new(user, true, form) + end + + it 'does not show option to skip setting up piv/cac' do + expect(rendered).not_to have_button(t('mfa.skip')) + end + context 'user has sufficient factors' do + let(:user) { create(:user, :fully_registered) } + it 'renders a link to cancel and go back to the account page' do - user = create(:user, :fully_registered) - allow(view).to receive(:current_user).and_return(user) - allow(view).to receive(:user_session).and_return(signing_up: false) - allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(false) - form = OpenStruct.new - @presenter = PivCacAuthenticationSetupPresenter.new(user, true, form) + expect(rendered).to have_link(t('links.cancel'), href: account_path) + end - render + context 'user is in the process of setting up multiple MFAs' do + let(:in_multi_mfa_selection_flow) { true } - expect(rendered).to have_link(t('links.cancel'), href: account_path) + it 'renders a link to choose a different option' do + expect(rendered).to have_link( + t('two_factor_authentication.choose_another_option'), + href: authentication_methods_setup_path, + ) + end end end context 'user is setting up 2FA' do - it 'renders a link to choose a different option' do - user = create(:user) - allow(view).to receive(:current_user).and_return(user) - allow(view).to receive(:user_session).and_return(signing_up: true) - form = OpenStruct.new - @presenter = PivCacAuthenticationSetupPresenter.new(user, false, form) - - render + let(:user) { create(:user) } + it 'renders a link to choose a different option' do expect(rendered).to have_link( t('two_factor_authentication.choose_another_option'), href: authentication_methods_setup_path, ) end end + + context 'when adding piv cac after 2fa' do + let(:user_session) { { add_piv_cac_after_2fa: true } } + + it 'shows option to skip setting up piv/cac' do + expect(rendered).to have_button(t('mfa.skip')) + end + + it 'renders a link to cancel and sign out' do + expect(rendered).to have_link(t('links.cancel'), href: sign_out_path) + end + + context 'when SP requires PIV/CAC' do + before do + @piv_cac_required = true + end + + it 'does not show option to skip setting up piv/cac' do + expect(rendered).not_to have_button(t('mfa.skip')) + end + + it 'renders a link to cancel and sign out' do + expect(rendered).to have_link(t('links.cancel'), href: sign_out_path) + end + end + end end