+
+<%= 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