diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index df9c15076d9..f4cd119bc25 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -4,6 +4,7 @@ class WebauthnVerificationController < ApplicationController include TwoFactorAuthenticatable before_action :check_sp_required_mfa + before_action :check_if_device_supports_platform_auth, only: :show before_action :confirm_webauthn_enabled, only: :show def show @@ -33,6 +34,17 @@ def confirm private + def check_if_device_supports_platform_auth + return unless user_session.has_key?(:platform_authenticator_available) + if platform_authenticator? && !device_supports_webauthn_platform? + redirect_to login_two_factor_options_url + end + end + + def device_supports_webauthn_platform? + user_session.delete(:platform_authenticator_available) == true + end + def handle_webauthn_result(result) if result.success? handle_valid_webauthn diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 7b019aa33da..651c8258c8d 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -121,6 +121,8 @@ def handle_valid_authentication user_id: current_user.id, email: auth_params[:email], ) + user_session[:platform_authenticator_available] = + params[:platform_authenticator_available] == 'true' redirect_to next_url_after_valid_authentication end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index d3b1602f3f1..ec8980a3a9f 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -1,6 +1,7 @@ module Users class TwoFactorAuthenticationController < ApplicationController include TwoFactorAuthenticatable + include ApplicationHelper include ActionView::Helpers::DateHelper before_action :check_remember_device_preference diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index 7539dcc2507..a21ec105f22 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -2,6 +2,8 @@ export { default as enrollWebauthnDevice } from './enroll-webauthn-device'; export { default as extractCredentials } from './extract-credentials'; export { default as verifyWebauthnDevice } from './verify-webauthn-device'; export { default as isExpectedWebauthnError } from './is-expected-error'; +export { default as isWebauthnPlatformAuthenticatorAvailable } from './is-webauthn-platform-authenticator-available'; +export { default as isWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; export * from './converters'; export type { VerifyCredentialDescriptor } from './verify-webauthn-device'; diff --git a/app/javascript/packs/platform-authenticator-available.ts b/app/javascript/packs/platform-authenticator-available.ts new file mode 100644 index 00000000000..e77e7195b70 --- /dev/null +++ b/app/javascript/packs/platform-authenticator-available.ts @@ -0,0 +1,20 @@ +import { + isWebauthnPlatformAuthenticatorAvailable, + isWebauthnPasskeySupported, +} from '@18f/identity-webauthn'; + +async function platformAuthenticatorAvailable() { + const platformAuthenticatorAvailableInput = document.getElementById( + 'platform_authenticator_available', + ) as HTMLInputElement; + if (!platformAuthenticatorAvailableInput) { + return; + } + if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) { + platformAuthenticatorAvailableInput.value = 'true'; + } else { + platformAuthenticatorAvailableInput.value = 'false'; + } +} + +platformAuthenticatorAvailable(); diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index f4550fd95ac..48c66b23065 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -47,6 +47,7 @@ }, }, ) %> + <%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %> <%= f.submit t('links.sign_in'), full_width: true, wide: false %> <% end %> <% if @ial && desktop_device? %> @@ -86,3 +87,5 @@ <% end %> +<%= javascript_packs_tag_once('platform-authenticator-available') %> + diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index 622e104260f..e5e924b1183 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -184,12 +184,17 @@ def index expect(response).to redirect_to login_two_factor_webauthn_path(platform: false) end - it 'passes the platform parameter if the user has a platform autheticator' do - controller.current_user.webauthn_configurations.first.update!(platform_authenticator: true) + context 'when platform_authenticator' do + before do + controller.current_user.webauthn_configurations. + first.update!(platform_authenticator: true) + end - get :show + it 'passes the platform parameter if the user has a platform autheticator' do + get :show - expect(response).to redirect_to login_two_factor_webauthn_path(platform: true) + expect(response).to redirect_to login_two_factor_webauthn_path(platform: true) + 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 4f297b4941a..56a86cf4b21 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -587,7 +587,6 @@ def attempt_to_bypass_2fa context 'sign in' do it 'allows user to be signed in without issue' do mock_webauthn_verification_challenge - sign_in_user(webauthn_configuration.user) mock_successful_webauthn_authentication { click_webauthn_authenticate_button } diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 7567ee11224..7b1f67bae47 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -83,11 +83,27 @@ let(:user) { create(:user, :fully_registered, :with_webauthn_platform) } context 'with javascript enabled', :js do - it 'displays the authenticator option' do - sign_in_user(user) - click_on t('two_factor_authentication.login_options_link_text') + context ' with device that supports authenticator' do + it 'displays the authenticator option' do + sign_in_user(user) + click_on t('two_factor_authentication.login_options_link_text') - expect(webauthn_option_hidden?).to eq(false) + expect(webauthn_option_hidden?).to eq(false) + end + end + + context 'with device that doesnt support authenticator' do + it 'redirects to options page on sign in and shows the option' do + email ||= user.email_addresses.first.email + password = user.password + allow(UserMailer).to receive(:new_device_sign_in).and_call_original + visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'false') + fill_in_credentials_and_submit(email, password) + continue_as(email, password) + expect(current_path).to eq(login_two_factor_options_path) + expect(webauthn_option_hidden?).to eq(false) + end end end diff --git a/spec/features/webauthn/sign_in_spec.rb b/spec/features/webauthn/sign_in_spec.rb index bd9440c192e..69f86e4c0fc 100644 --- a/spec/features/webauthn/sign_in_spec.rb +++ b/spec/features/webauthn/sign_in_spec.rb @@ -75,9 +75,33 @@ mock_webauthn_verification_challenge sign_in_user(user) + expect(current_url).to eq(login_two_factor_webauthn_url(platform: true)) mock_cancelled_webauthn_authentication { click_webauthn_authenticate_button } expect(page).to have_content(t('two_factor_authentication.webauthn_platform_header_text')) end + + context 'with device that doesnt support authenticator' do + before do + email ||= user.email_addresses.first.email + password = user.password + allow(UserMailer).to receive(:new_device_sign_in).and_call_original + visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'false') + fill_in_credentials_and_submit(email, password) + continue_as(email, password) + end + + it 'redirects to options page on sign in' do + expect(current_path).to eq(login_two_factor_options_path) + end + + it 'allows user to go to options page and still select webauthn as their option' do + expect(current_path).to eq(login_two_factor_options_path) + select_2fa_option('webauthn_platform', visible: :all) + click_continue + expect(current_url).to eq(login_two_factor_webauthn_url(platform: true)) + end + end end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 2c43879c70e..ed1e55d8d72 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -2,6 +2,7 @@ module Features module SessionHelper + include JavascriptDriverHelper include PersonalKeyHelper VALID_PASSWORD = 'Val!d Pass w0rd'.freeze @@ -50,6 +51,7 @@ def sign_up_and_2fa_ial1_user def signin(email, password) allow(UserMailer).to receive(:new_device_sign_in).and_call_original visit new_user_session_path + set_hidden_field('platform_authenticator_available', 'true') fill_in_credentials_and_submit(email, password) continue_as(email, password) end @@ -729,5 +731,14 @@ def expect_branded_experience def acknowledge_backup_code_confirmation click_on t('two_factor_authentication.backup_codes.saved_backup_codes') end + + def set_hidden_field(id, value) + input = first("input##{id}", visible: false) + if javascript_enabled? + input.execute_script("this.value = #{value.to_json}") + else + input.set(value) + end + end end end