diff --git a/app/assets/images/webauthn-mismatch/webauthn-checked.svg b/app/assets/images/webauthn-mismatch/webauthn-checked.svg new file mode 100644 index 00000000000..1c6efd9a372 --- /dev/null +++ b/app/assets/images/webauthn-mismatch/webauthn-checked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/webauthn-mismatch/webauthn-platform-checked.svg b/app/assets/images/webauthn-mismatch/webauthn-platform-checked.svg new file mode 100644 index 00000000000..f3aa64c183a --- /dev/null +++ b/app/assets/images/webauthn-mismatch/webauthn-platform-checked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/webauthn-mismatch/webauthn-platform-unchecked.svg b/app/assets/images/webauthn-mismatch/webauthn-platform-unchecked.svg new file mode 100644 index 00000000000..8383e9280d2 --- /dev/null +++ b/app/assets/images/webauthn-mismatch/webauthn-platform-unchecked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/webauthn-mismatch/webauthn-unchecked.svg b/app/assets/images/webauthn-mismatch/webauthn-unchecked.svg new file mode 100644 index 00000000000..f70cb8438ff --- /dev/null +++ b/app/assets/images/webauthn-mismatch/webauthn-unchecked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/controllers/concerns/mfa_deletion_concern.rb b/app/controllers/concerns/mfa_deletion_concern.rb new file mode 100644 index 00000000000..0f4c647aa2e --- /dev/null +++ b/app/controllers/concerns/mfa_deletion_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module MfaDeletionConcern + include RememberDeviceConcern + + def handle_successful_mfa_deletion(event_type:) + create_user_event(event_type) + revoke_remember_device(current_user) + event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) + PushNotification::HttpPush.deliver(event) + nil + end +end diff --git a/app/controllers/users/webauthn_setup_mismatch_controller.rb b/app/controllers/users/webauthn_setup_mismatch_controller.rb new file mode 100644 index 00000000000..719d45dce24 --- /dev/null +++ b/app/controllers/users/webauthn_setup_mismatch_controller.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Users + class WebauthnSetupMismatchController < ApplicationController + include MfaSetupConcern + include MfaDeletionConcern + include SecureHeadersConcern + include ReauthenticationRequiredConcern + + before_action :confirm_user_authenticated_for_2fa_setup + before_action :apply_secure_headers_override + before_action :confirm_recently_authenticated_2fa + before_action :validate_session_mismatch_id + + def show + analytics.webauthn_setup_mismatch_visited( + configuration_id: configuration.id, + platform_authenticator: platform_authenticator?, + ) + + @presenter = WebauthnSetupMismatchPresenter.new(configuration:) + end + + def update + analytics.webauthn_setup_mismatch_submitted( + configuration_id: configuration.id, + platform_authenticator: platform_authenticator?, + confirmed_mismatch: true, + ) + + redirect_to next_setup_path || after_mfa_setup_path + end + + def destroy + result = ::TwoFactorAuthentication::WebauthnDeleteForm.new( + user: current_user, + configuration_id: webauthn_mismatch_id, + skip_multiple_mfa_validation: in_multi_mfa_selection_flow?, + ).submit + + analytics.webauthn_setup_mismatch_submitted(**result.to_h, confirmed_mismatch: false) + + if result.success? + handle_successful_mfa_deletion(event_type: :webauthn_key_removed) + redirect_to retry_setup_url + else + flash.now[:error] = result.first_error_message + @presenter = WebauthnSetupMismatchPresenter.new(configuration:) + render :show + end + end + + private + + def retry_setup_url + # These are intentionally inverted: if the authenticator was set up as a platform + # authenticator but was flagged as a mismatch, it implies that the user had originally + # intended to add a security key. + if platform_authenticator? + webauthn_setup_url + else + webauthn_setup_url(platform: true) + end + end + + def webauthn_mismatch_id + user_session[:webauthn_mismatch_id] + end + + def configuration + return @configuration if defined?(@configuration) + @configuration = current_user.webauthn_configurations.find_by(id: webauthn_mismatch_id) + end + + def validate_session_mismatch_id + return if configuration.present? + redirect_to next_setup_path || after_mfa_setup_path + end + + delegate :platform_authenticator?, to: :configuration + end +end diff --git a/app/forms/two_factor_authentication/webauthn_delete_form.rb b/app/forms/two_factor_authentication/webauthn_delete_form.rb index 8d965a66dd5..ee282c0898d 100644 --- a/app/forms/two_factor_authentication/webauthn_delete_form.rb +++ b/app/forms/two_factor_authentication/webauthn_delete_form.rb @@ -10,9 +10,10 @@ class WebauthnDeleteForm validate :validate_configuration_exists validate :validate_has_multiple_mfa - def initialize(user:, configuration_id:) + def initialize(user:, configuration_id:, skip_multiple_mfa_validation: false) @user = user @configuration_id = configuration_id + @skip_multiple_mfa_validation = skip_multiple_mfa_validation end def submit @@ -34,6 +35,10 @@ def configuration private + attr_reader :skip_multiple_mfa_validation + + alias_method :skip_multiple_mfa_validation?, :skip_multiple_mfa_validation + def validate_configuration_exists return if configuration.present? errors.add( @@ -44,7 +49,10 @@ def validate_configuration_exists end def validate_has_multiple_mfa - return if !configuration || MfaPolicy.new(user).multiple_factors_enabled? + return if skip_multiple_mfa_validation? || + !configuration || + MfaPolicy.new(user).multiple_factors_enabled? + errors.add( :configuration_id, :only_method, diff --git a/app/presenters/webauthn_setup_mismatch_presenter.rb b/app/presenters/webauthn_setup_mismatch_presenter.rb new file mode 100644 index 00000000000..22187cf1c35 --- /dev/null +++ b/app/presenters/webauthn_setup_mismatch_presenter.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class WebauthnSetupMismatchPresenter + include ActionView::Helpers::TranslationHelper + + attr_reader :configuration + + def initialize(configuration:) + @configuration = configuration + end + + def heading + if platform_authenticator? + t('webauthn_setup_mismatch.heading.webauthn_platform') + else + t('webauthn_setup_mismatch.heading.webauthn') + end + end + + def description + if platform_authenticator? + t('webauthn_setup_mismatch.description.webauthn_platform') + else + t('webauthn_setup_mismatch.description.webauthn') + end + end + + def correct_image_path + if platform_authenticator? + 'webauthn-mismatch/webauthn-platform-checked.svg' + else + 'webauthn-mismatch/webauthn-checked.svg' + end + end + + def incorrect_image_path + if platform_authenticator? + 'webauthn-mismatch/webauthn-unchecked.svg' + else + 'webauthn-mismatch/webauthn-platform-unchecked.svg' + end + end + + private + + delegate :platform_authenticator?, to: :configuration +end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 377a3f4b74d..89b6ffd2c57 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -7706,7 +7706,54 @@ def webauthn_platform_recommended_visited track_event(:webauthn_platform_recommended_visited) end - # @param [Hash] platform_authenticator + # @param [Boolean] platform_authenticator Whether authentication method was registered as platform + # authenticator + # @param [Number] configuration_id Database ID of WebAuthn configuration + # @param [Boolean] confirmed_mismatch Whether user chose to confirm and continue with interpreted + # platform attachment + # @param [Boolean] success Whether the deletion was successful, if user chose to undo interpreted + # platform attachment + # @param [Hash] error_details Details for errors that occurred in unsuccessful deletion + # User submitted confirmation screen after setting up WebAuthn with transports mismatched with the + # expected platform attachment + def webauthn_setup_mismatch_submitted( + configuration_id:, + platform_authenticator:, + confirmed_mismatch:, + success: nil, + error_details: nil, + **extra + ) + track_event( + :webauthn_setup_mismatch_submitted, + configuration_id:, + platform_authenticator:, + confirmed_mismatch:, + success:, + error_details:, + **extra, + ) + end + + # @param [Boolean] platform_authenticator Whether authentication method was registered as platform + # authenticator + # @param [Number] configuration_id Database ID of WebAuthn configuration + # User visited confirmation screen after setting up WebAuthn with transports mismatched with the + # expected platform attachment + def webauthn_setup_mismatch_visited( + configuration_id:, + platform_authenticator:, + **extra + ) + track_event( + :webauthn_setup_mismatch_visited, + configuration_id:, + platform_authenticator:, + **extra, + ) + end + + # @param [Boolean] platform_authenticator # @param [Boolean] success # @param [Hash, nil] errors # @param [Boolean] in_account_creation_flow Whether user is going through account creation flow diff --git a/app/views/users/webauthn_setup_mismatch/show.html.erb b/app/views/users/webauthn_setup_mismatch/show.html.erb new file mode 100644 index 00000000000..2891e30b4f3 --- /dev/null +++ b/app/views/users/webauthn_setup_mismatch/show.html.erb @@ -0,0 +1,42 @@ +<% self.title = @presenter.heading %> + +
+ <%= image_tag( + @presenter.correct_image_path, + width: 104, + height: 116, + alt: '', + aria: { hidden: true }, + ) %> + <%= image_tag( + @presenter.incorrect_image_path, + width: 64, + height: 71, + class: 'margin-left-2', + alt: '', + aria: { hidden: true }, + ) %> +
+ +<%= render PageHeadingComponent.new.with_content(@presenter.heading) %> + +

<%= @presenter.description %>

+ +

<%= t('webauthn_setup_mismatch.description_undo') %> + +

+ <%= render ButtonComponent.new( + url: webauthn_setup_mismatch_url, + method: :patch, + big: true, + wide: true, + ).with_content(t('forms.buttons.continue')) %> +
+ +<%= render ButtonComponent.new( + url: webauthn_setup_mismatch_url, + method: :delete, + big: true, + wide: true, + outline: true, + ).with_content(t('webauthn_setup_mismatch.undo')) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index d8a6e5f7916..85a5c2fdbd2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2044,6 +2044,12 @@ webauthn_platform_recommended.cta: Set up face or touch unlock webauthn_platform_recommended.description_save_time: Save time by using your face, fingerprint, password, or another method to access your account. This method is faster than receiving a one-time code through text or voice message. webauthn_platform_recommended.heading: Set up face or touch unlock for a quick and easy sign in webauthn_platform_recommended.skip: Skip +webauthn_setup_mismatch.description_undo: Click “Undo” to remove this option. +webauthn_setup_mismatch.description.webauthn: We noticed you’re using a security key instead of face or touch unlock. Click “Continue” to use your security key to sign in from now on. +webauthn_setup_mismatch.description.webauthn_platform: We noticed you’re using face or touch unlock instead of a security key. Click “Continue” to use face or touch unlock to sign in from now on. +webauthn_setup_mismatch.heading.webauthn: Security key detected +webauthn_setup_mismatch.heading.webauthn_platform: Face or touch unlock detected +webauthn_setup_mismatch.undo: Undo zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: A word by itself is easy to guess zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Add another word or two. Uncommon words are better zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: All-uppercase is almost as easy to guess as all-lowercase diff --git a/config/locales/es.yml b/config/locales/es.yml index 5cc9d9b3fce..fd63d3dc7c5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2056,6 +2056,12 @@ webauthn_platform_recommended.cta: Set up face or touch unlock webauthn_platform_recommended.description_save_time: Save time by using your face, fingerprint, password, or another method to access your account. This method is faster than receiving a one-time code through text or voice message. webauthn_platform_recommended.heading: Set up face or touch unlock for a quick and easy sign in webauthn_platform_recommended.skip: Skip +webauthn_setup_mismatch.description_undo: Haga clic en “Deshacer” para quitar esta opción. +webauthn_setup_mismatch.description.webauthn: Sabemos que está usando una clave de seguridad en lugar de desbloqueo facial o táctil. Haga clic en “Continuar” para iniciar sesión con su clave de seguridad de aquí en adelante. +webauthn_setup_mismatch.description.webauthn_platform: Sabemos que está usando desbloqueo facial o táctil en lugar de una clave de seguridad. Haga clic en “Continuar” para iniciar sesión con desbloqueo facial o táctil de aquí en adelante. +webauthn_setup_mismatch.heading.webauthn: Se detectó clave de seguridad +webauthn_setup_mismatch.heading.webauthn_platform: Se detectó desbloqueo facial o táctil +webauthn_setup_mismatch.undo: Deshacer zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: Una sola palabra es fácil de adivinar. zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Añada otra palabra o dos. Es mejor usar palabras poco comunes. zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Todo en mayúsculas es casi tan fácil de adivinar como todo en minúsculas. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d5b3c779bb2..9741c609887 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2044,6 +2044,12 @@ webauthn_platform_recommended.cta: Set up face or touch unlock webauthn_platform_recommended.description_save_time: Save time by using your face, fingerprint, password, or another method to access your account. This method is faster than receiving a one-time code through text or voice message. webauthn_platform_recommended.heading: Set up face or touch unlock for a quick and easy sign in webauthn_platform_recommended.skip: Skip +webauthn_setup_mismatch.description_undo: Cliquez sur « Annuler » pour supprimer cette option. +webauthn_setup_mismatch.description.webauthn: Nous avons remarqué que vous utilisiez une clé de sécurité au lieu du déverrouillage facial ou tactile. Cliquez sur « Suite » pour utiliser votre clé de sécurité afin de vous connecter à partir de maintenant. +webauthn_setup_mismatch.description.webauthn_platform: Nous avons remarqué que vous utilisiez le déverrouillage facial ou tactile au lieu d’une clé de sécurité. Cliquez sur « Suite » pour utiliser le déverrouillage facial ou tactile afin de vous connecter à partir de maintenant. +webauthn_setup_mismatch.heading.webauthn: Clé de sécurité détectée +webauthn_setup_mismatch.heading.webauthn_platform: Déverrouillage facial ou tactile détecté +webauthn_setup_mismatch.undo: Annuler zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: Un mot seul est facile à deviner zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Ajoutez un ou deux autres mots. Il est préférable d’utiliser des mots peu communs. zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Tout en majuscules est presque aussi facile à deviner que tout en minuscules. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 6d50599de34..a16d5822c49 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -2057,6 +2057,12 @@ webauthn_platform_recommended.cta: Set up face or touch unlock webauthn_platform_recommended.description_save_time: Save time by using your face, fingerprint, password, or another method to access your account. This method is faster than receiving a one-time code through text or voice message. webauthn_platform_recommended.heading: Set up face or touch unlock for a quick and easy sign in webauthn_platform_recommended.skip: Skip +webauthn_setup_mismatch.description_undo: 点击“撤消”可删除此选项。 +webauthn_setup_mismatch.description.webauthn: 我们注意到您正在使用安全密钥而不是人脸或触摸解锁。点击“继续”即可从现在开始使用您的安全密钥登录。 +webauthn_setup_mismatch.description.webauthn_platform: 我们注意到您正在使用人脸或触摸解锁,而不是安全密钥。点击“继续”即可从现在开始使用人脸或触摸解锁登录。 +webauthn_setup_mismatch.heading.webauthn: 发现安全密钥 +webauthn_setup_mismatch.heading.webauthn_platform: 发现人脸或触摸解锁 +webauthn_setup_mismatch.undo: 撤消 zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: 单字容易被人猜出 zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: 再加一两个字不常见的字更好 zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: 都是大写几乎和都是小写一样容易被人猜出 diff --git a/config/routes.rb b/config/routes.rb index 4a45d208115..5114f550a78 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -250,6 +250,10 @@ get '/webauthn_setup' => 'users/webauthn_setup#new', as: :webauthn_setup patch '/webauthn_setup' => 'users/webauthn_setup#confirm' + get '/webauthn_setup_mismatch' => 'users/webauthn_setup_mismatch#show' + patch '/webauthn_setup_mismatch' => 'users/webauthn_setup_mismatch#update' + delete '/webauthn_setup_mismatch' => 'users/webauthn_setup_mismatch#destroy' + get '/authenticator_setup' => 'users/totp_setup#new' patch '/authenticator_setup' => 'users/totp_setup#confirm' diff --git a/spec/controllers/concerns/mfa_deletion_concern_spec.rb b/spec/controllers/concerns/mfa_deletion_concern_spec.rb new file mode 100644 index 00000000000..0193cbffaae --- /dev/null +++ b/spec/controllers/concerns/mfa_deletion_concern_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe MfaDeletionConcern do + controller ApplicationController do + include MfaDeletionConcern + end + + let(:user) { create(:user, :fully_registered) } + + before do + stub_sign_in(user) + end + + describe '#handle_successful_mfa_deletion' do + let(:event_type) { Event.event_types.keys.sample.to_sym } + subject(:result) { controller.handle_successful_mfa_deletion(event_type:) } + + it 'does not return a value' do + expect(result).to be_nil + end + + it 'creates user event using event_type argument' do + expect(controller).to receive(:create_user_event).with(event_type) + + result + end + + it 'revokes remembered device for user' do + expect(controller).to receive(:revoke_remember_device).with(user) + + result + end + + it 'sends risc push notification' do + expect(PushNotification::HttpPush).to receive(:deliver) do |event| + expect(event.user).to eq(user) + end + + result + end + end +end diff --git a/spec/controllers/users/webauthn_setup_mismatch_controller_spec.rb b/spec/controllers/users/webauthn_setup_mismatch_controller_spec.rb new file mode 100644 index 00000000000..9b2042b11b0 --- /dev/null +++ b/spec/controllers/users/webauthn_setup_mismatch_controller_spec.rb @@ -0,0 +1,231 @@ +require 'rails_helper' + +RSpec.describe Users::WebauthnSetupMismatchController do + let(:user) { create(:user, :fully_registered, :with_webauthn) } + let(:webauthn_mismatch_id) { user.webauthn_configurations.take.id } + + before do + stub_sign_in(user) if user + controller.user_session&.[]=(:webauthn_mismatch_id, webauthn_mismatch_id) + end + + shared_examples 'a validated mismatch controller action' do + it 'applies secure headers override' do + expect(controller).to receive(:apply_secure_headers_override) + + response + end + + context 'user is not signed in' do + let(:user) { nil } + + it 'redirects user to sign in' do + expect(response).to redirect_to(new_user_session_url) + end + end + + context 'user is not fully-authenticated' do + let(:user) { nil } + + before do + stub_sign_in_before_2fa(create(:user, :fully_registered)) + end + + it 'redirects user to authenticate' do + expect(response).to redirect_to(user_two_factor_authentication_url) + end + end + + context 'user is not recently authenticated' do + before do + expire_reauthn_window + end + + it 'redirects user to authenticate' do + expect(response).to redirect_to(login_two_factor_options_url) + end + end + + context 'session configuration id is missing' do + let(:webauthn_mismatch_id) { nil } + + it 'redirects to next setup path' do + expect(response).to redirect_to(account_url) + end + end + + context 'session configuration id is invalid' do + let(:webauthn_mismatch_id) { 1 } + + it 'redirects to next setup path' do + expect(response).to redirect_to(account_url) + end + end + end + + describe '#show' do + subject(:response) { get :show } + + it_behaves_like 'a validated mismatch controller action' + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_visited, + configuration_id: webauthn_mismatch_id, + platform_authenticator: false, + ) + end + + it 'assigns presenter instance variable for view' do + response + + presenter = assigns(:presenter) + expect(presenter).to be_kind_of(WebauthnSetupMismatchPresenter) + expect(presenter.configuration.id).to eq(webauthn_mismatch_id) + end + + context 'with platform authenticator' do + let(:user) { create(:user, :fully_registered, :with_webauthn_platform) } + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_visited, + configuration_id: webauthn_mismatch_id, + platform_authenticator: true, + ) + end + end + end + + describe '#update' do + subject(:response) { patch :update } + + it_behaves_like 'a validated mismatch controller action' + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_submitted, + configuration_id: webauthn_mismatch_id, + platform_authenticator: false, + confirmed_mismatch: true, + ) + end + + it 'redirects to next setup path' do + expect(response).to redirect_to(account_url) + end + + context 'with platform authenticator' do + let(:user) { create(:user, :fully_registered, :with_webauthn_platform) } + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_submitted, + configuration_id: webauthn_mismatch_id, + platform_authenticator: true, + confirmed_mismatch: true, + ) + end + end + end + + describe '#destroy' do + subject(:response) { delete :destroy } + + it_behaves_like 'a validated mismatch controller action' + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_submitted, + success: true, + configuration_id: webauthn_mismatch_id, + platform_authenticator: false, + confirmed_mismatch: false, + ) + end + + it 'invalidates deleted authenticator' do + expect(controller).to receive(:handle_successful_mfa_deletion) + .with(event_type: :webauthn_key_removed) + + response + end + + context 'if deletion is unsuccessful' do + before do + user.phone_configurations.delete_all + end + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_submitted, + success: false, + error_details: { configuration_id: { only_method: true } }, + configuration_id: webauthn_mismatch_id, + platform_authenticator: false, + confirmed_mismatch: false, + ) + end + + it 'assigns presenter instance variable for view' do + response + + presenter = assigns(:presenter) + expect(presenter).to be_kind_of(WebauthnSetupMismatchPresenter) + expect(presenter.configuration.id).to eq(webauthn_mismatch_id) + end + + it 'flashes error message' do + response + + expect(flash.now[:error]).to eq(t('errors.manage_authenticator.remove_only_method_error')) + end + + it 'renders new view' do + expect(response).to render_template(:show) + end + end + + context 'with platform authenticator' do + let(:user) { create(:user, :fully_registered, :with_webauthn_platform) } + + it 'logs analytics event' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + :webauthn_setup_mismatch_submitted, + success: true, + configuration_id: webauthn_mismatch_id, + platform_authenticator: true, + confirmed_mismatch: false, + ) + end + end + end +end diff --git a/spec/forms/two_factor_authentication/webauthn_delete_form_spec.rb b/spec/forms/two_factor_authentication/webauthn_delete_form_spec.rb index f0c210aceb9..be7244a503a 100644 --- a/spec/forms/two_factor_authentication/webauthn_delete_form_spec.rb +++ b/spec/forms/two_factor_authentication/webauthn_delete_form_spec.rb @@ -4,7 +4,10 @@ let(:user) { create(:user) } let(:configuration) { create(:webauthn_configuration, user:) } let(:configuration_id) { configuration&.id } - let(:form) { described_class.new(user:, configuration_id:) } + let(:skip_multiple_mfa_validation) {} + let(:form) do + described_class.new(user:, configuration_id:, **{ skip_multiple_mfa_validation: }.compact) + end describe '#submit' do let(:result) { form.submit } @@ -95,6 +98,19 @@ ) end + context 'with skipped multiple mfa validation' do + let(:skip_multiple_mfa_validation) { true } + + it 'returns a successful result' do + expect(result.success?).to eq(true) + expect(result.to_h).to eq( + success: true, + configuration_id:, + platform_authenticator: false, + ) + end + end + context 'with platform authenticator' do let(:configuration) do create(:webauthn_configuration, :platform_authenticator, user:) diff --git a/spec/presenters/webauthn_setup_mismatch_presenter_spec.rb b/spec/presenters/webauthn_setup_mismatch_presenter_spec.rb new file mode 100644 index 00000000000..7fafd793ecc --- /dev/null +++ b/spec/presenters/webauthn_setup_mismatch_presenter_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +RSpec.describe WebauthnSetupMismatchPresenter do + subject(:presenter) { described_class.new(configuration:) } + let(:platform_authenticator) {} + let(:configuration) { create(:webauthn_configuration, platform_authenticator:) } + + describe '#heading' do + subject(:heading) { presenter.heading } + + context 'with non-platform authenticator' do + let(:platform_authenticator) { false } + + it { is_expected.to eq(t('webauthn_setup_mismatch.heading.webauthn')) } + end + + context 'with platform authenticator' do + let(:platform_authenticator) { true } + + it { is_expected.to eq(t('webauthn_setup_mismatch.heading.webauthn_platform')) } + end + end + + describe '#description' do + subject(:description) { presenter.description } + + context 'with non-platform authenticator' do + let(:platform_authenticator) { false } + + it { is_expected.to eq(t('webauthn_setup_mismatch.description.webauthn')) } + end + + context 'with platform authenticator' do + let(:platform_authenticator) { true } + + it { is_expected.to eq(t('webauthn_setup_mismatch.description.webauthn_platform')) } + end + end + + describe '#correct_image_path' do + subject(:correct_image_path) { presenter.correct_image_path } + + context 'with non-platform authenticator' do + let(:platform_authenticator) { false } + + it { is_expected.to eq('webauthn-mismatch/webauthn-checked.svg') } + end + + context 'with platform authenticator' do + let(:platform_authenticator) { true } + + it { is_expected.to eq('webauthn-mismatch/webauthn-platform-checked.svg') } + end + end + + describe '#incorrect_image_path' do + subject(:incorrect_image_path) { presenter.incorrect_image_path } + + context 'with non-platform authenticator' do + let(:platform_authenticator) { false } + + it { is_expected.to eq('webauthn-mismatch/webauthn-platform-unchecked.svg') } + end + + context 'with platform authenticator' do + let(:platform_authenticator) { true } + + it { is_expected.to eq('webauthn-mismatch/webauthn-unchecked.svg') } + end + end +end diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index d03875df7cf..ce03cfd2f8c 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -37,6 +37,12 @@ def stub_sign_in_before_2fa(user = build(:user, password: VALID_PASSWORD)) controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION] = true end + def expire_reauthn_window + controller.user_session[:auth_events].each do |auth_event| + auth_event['at'] -= IdentityConfig.store.reauthn_window.seconds + end + end + def stub_verify_steps_one_and_two( user, applicant: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE diff --git a/spec/views/users/webauthn_setup_mismatch/show.html.erb_spec.rb b/spec/views/users/webauthn_setup_mismatch/show.html.erb_spec.rb new file mode 100644 index 00000000000..b9e6bbfbfd5 --- /dev/null +++ b/spec/views/users/webauthn_setup_mismatch/show.html.erb_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe 'users/webauthn_setup_mismatch/show.html.erb' do + subject(:rendered) { render } + let(:configuration) { create(:webauthn_configuration) } + let(:presenter) { WebauthnSetupMismatchPresenter.new(configuration:) } + + before do + assign(:presenter, presenter) + end + + it 'sets title from presenter heading' do + expect(view).to receive(:title=).with(presenter.heading) + + render + end + + it 'renders heading from presenter heading' do + expect(rendered).to have_css('h1', text: presenter.heading) + end + + it 'renders description from presenter description' do + expect(rendered).to have_css('p', text: presenter.description) + end + + it 'renders buttons to continue or undo' do + expect(rendered).to have_button(t('forms.buttons.continue')) + expect(rendered).to have_button(t('webauthn_setup_mismatch.undo')) + end +end