From ac5bc0de8f75334d31a3dfdce15485ab190d14fe Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 8 Oct 2024 10:43:02 -0400 Subject: [PATCH 01/62] add desktop ab test information to form --- app/controllers/users/webauthn_setup_controller.rb | 4 +++- app/forms/webauthn_setup_form.rb | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 6a80cfda56b..0a576d624ac 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -61,12 +61,14 @@ def confirm ) result = form.submit(confirm_params) @platform_authenticator = form.platform_authenticator? + @desktop_ab_test_bucket = form.desktop_ab_test_bucket? @presenter = WebauthnSetupPresenter.new( current_user: current_user, user_fully_authenticated: user_fully_authenticated?, user_opted_remember_device_cookie: user_opted_remember_device_cookie, remember_device_default: remember_device_default, platform_authenticator: @platform_authenticator, + desktop_ab_test_bucket: @desktop_ab_test_bucket, url_options:, ) properties = result.to_h.merge(analytics_properties) @@ -86,7 +88,7 @@ def validate_existing_platform_authenticator if platform_authenticator? && in_account_creation_flow? && current_user.webauthn_configurations.platform_authenticators.present? redirect_to authentication_methods_setup_path - end + end end def platform_authenticator? diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index 9c593c9aaaf..fb2a77e8e51 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -25,6 +25,7 @@ def initialize(user:, user_session:, device_name:) @authenticator_data_flags = nil @protocol = nil @device_name = device_name + @desktop_ab_test_bucket = false end def submit(params) @@ -48,6 +49,10 @@ def platform_authenticator? !!@platform_authenticator end + def desktop_ab_test_bucket? + platform_authenticator? && desktop_ab_test_bucket + end + def generic_error_message if platform_authenticator? I18n.t('errors.webauthn_platform_setup.general_error') @@ -63,12 +68,14 @@ def generic_error_message attr_reader :success, :transports, :invalid_transports, :protocol attr_accessor :user, :challenge, :attestation_object, :client_data_json, - :name, :platform_authenticator, :authenticator_data_flags, :device_name + :name, :platform_authenticator, :authenticator_data_flags, :device_name, + :desktop_ab_test_bucket def consume_parameters(params) @attestation_object = params[:attestation_object] @client_data_json = params[:client_data_json] @platform_authenticator = (params[:platform_authenticator].to_s == 'true') + @desktop_ab_test_bucket = params[:ab_test_bucket] @name = @platform_authenticator ? @device_name : params[:name] @authenticator_data_flags = process_authenticator_data_value( params[:authenticator_data_value], From 9b8a7c4b8f90890a8df0eef8a8c4fbba410a9b5e Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 9 Oct 2024 15:46:18 -0400 Subject: [PATCH 02/62] add a/b test configuration --- config/application.yml.default | 1 + config/initializers/ab_tests.rb | 16 ++++++++++++++++ lib/identity_config.rb | 1 + 3 files changed, 18 insertions(+) diff --git a/config/application.yml.default b/config/application.yml.default index ef8d81c4a72..83b271c45c8 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -338,6 +338,7 @@ session_timeout_warning_seconds: 150 session_total_duration_timeout_in_minutes: 720 short_term_phone_otp_max_attempt_window_in_seconds: 10 short_term_phone_otp_max_attempts: 2 +show_desktop_ft_unlock_setup_option_percent_tested: 0 show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false sign_in_recaptcha_percent_tested: 0 diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 5a970f79907..80fb8b0f96f 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -81,4 +81,20 @@ def self.all SecureRandom.alphanumeric(8) end end.freeze + + DESKTOP_FT_UNLOCK_SETUP = AbTest.new( + experiment_name: 'Desktop F/T unlock setup', + should_log: [ + 'WebAuthn Setup Visited', + 'Multi-Factor Authentication Setup', + ].to_set, + buckets: { desktop_ft_unlock: + IdentityConfig.store.show_desktop_ft_unlock_setup_option_percent_tested }, + ) do |user:, user_session:, **| + if user_session&.[](:platform_authenticator_available) == false + nil + else + user.uuid + end + end.freeze end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 281618894f5..6f9b29d9b92 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -389,6 +389,7 @@ def self.store config.add(:session_timeout_in_minutes, type: :integer) config.add(:session_timeout_warning_seconds, type: :integer) config.add(:session_total_duration_timeout_in_minutes, type: :integer) + config.add(:show_desktop_ft_unlock_setup_option_percent_tested, type: :integer) config.add(:show_unsupported_passkey_platform_authentication_setup, type: :boolean) config.add(:show_user_attribute_deprecation_warnings, type: :boolean) config.add(:short_term_phone_otp_max_attempts, type: :integer) From e4040c89a297e9eaa6ee2611b16e1c883f788fd5 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 10 Oct 2024 13:47:01 -0400 Subject: [PATCH 03/62] remove anything related to ab test bucket --- app/forms/webauthn_setup_form.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index fb2a77e8e51..9c593c9aaaf 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -25,7 +25,6 @@ def initialize(user:, user_session:, device_name:) @authenticator_data_flags = nil @protocol = nil @device_name = device_name - @desktop_ab_test_bucket = false end def submit(params) @@ -49,10 +48,6 @@ def platform_authenticator? !!@platform_authenticator end - def desktop_ab_test_bucket? - platform_authenticator? && desktop_ab_test_bucket - end - def generic_error_message if platform_authenticator? I18n.t('errors.webauthn_platform_setup.general_error') @@ -68,14 +63,12 @@ def generic_error_message attr_reader :success, :transports, :invalid_transports, :protocol attr_accessor :user, :challenge, :attestation_object, :client_data_json, - :name, :platform_authenticator, :authenticator_data_flags, :device_name, - :desktop_ab_test_bucket + :name, :platform_authenticator, :authenticator_data_flags, :device_name def consume_parameters(params) @attestation_object = params[:attestation_object] @client_data_json = params[:client_data_json] @platform_authenticator = (params[:platform_authenticator].to_s == 'true') - @desktop_ab_test_bucket = params[:ab_test_bucket] @name = @platform_authenticator ? @device_name : params[:name] @authenticator_data_flags = process_authenticator_data_value( params[:authenticator_data_value], From 8fe5a6a050bcc862791f402625eb7232b1d7a64f Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 10 Oct 2024 14:52:04 -0400 Subject: [PATCH 04/62] add passkey support on desktop --- .../webauthn/is-webauthn-passkey-supported.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index b496028322b..7ad06f597f0 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -4,6 +4,10 @@ const MINIMUM_IOS_VERSION = 16; const MINIMUM_ANDROID_VERSION = 9; +const MINIMUM_MACOS_VERSION = 13; + +const MINIMUM_WINDOWS_VERSION = 10 + function isQualifyingIOSDevice(): boolean { const match = navigator.userAgent.match(/iPhone; CPU iPhone OS (\d+)_/); const iOSVersion: null | number = match && Number(match[1]); @@ -24,7 +28,20 @@ function isQualifyingAndroidDevice(): boolean { ); } +function isQualifyingMacOSDesktopDevice(): boolean { + const match = navigator.userAgent.match(/Mac OS X/); + const macOsVersion: null | number = match && Number(match[1]); + return !!macOsVersion && macOsVersion >= MINIMUM_MACOS_VERSION; +} + +function isQualifyingWindowsDesktopDevice(): boolean { + const match = navigator.userAgent.match(/Windows/); + const windowsVersion: null | number = match && Number(match[1]); + return !!windowsVersion && windowsVersion >= MINIMUM_WINDOWS_VERSION; +} + const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - isQualifyingIOSDevice() || isQualifyingAndroidDevice(); + isQualifyingIOSDevice() || isQualifyingAndroidDevice() + || isQualifyingMacOSDesktopDevice() || isQualifyingWindowsDesktopDevice(); export default isWebauthnPasskeySupported; From a45733e06078657ee414af319c15570c6b538825 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 11 Oct 2024 13:30:54 -0400 Subject: [PATCH 05/62] remove device supported check --- app/javascript/packages/webauthn/index.ts | 2 +- .../is-webauthn-passkey-supported.spec.ts | 97 ------------------- .../webauthn/is-webauthn-passkey-supported.ts | 47 --------- 3 files changed, 1 insertion(+), 145 deletions(-) delete mode 100644 app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts delete mode 100644 app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index a21ec105f22..31f64399acc 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -3,7 +3,7 @@ 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/packages/webauthn/is-webauthn-passkey-supported.spec.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts deleted file mode 100644 index 53342b817e3..00000000000 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useDefineProperty } from '@18f/identity-test-helpers'; -import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; - -// Source (Adapted): https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state -const UNSUPPORTED_ANDROID_VERSION_UA = - 'Mozilla/5.0 (Linux; Android 8; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36'; - -// Source: https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state -const REDUCED_UNSUPPORTED_ANDROID_VERSION_UA = - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36'; - -// Source: https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state -const SUPPORTED_ANDROID_VERSION_CHROME_UA = - 'Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36'; - -// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox#mobile_and_tablet_indicators -const SUPPORTED_ANDROID_VERSION_FIREFOX_UA = - 'Mozilla/5.0 (Android 9; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'; - -// Source: https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md -const UNSUPPORTED_IOS_VERSION_CHROME_UA = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; - -// Source: https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md -const UNSUPPORTED_IOS_VERSION_SAFARI_UA = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/603.1.23 (KHTML, like Gecko) Version/10.0 Mobile/14E5239e Safari/602.1'; - -// Source (Adapted): https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md -const SUPPORTED_IOS_CHROME_VERSION_UA = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; - -// Source (Adapted): https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md -const SUPPORTED_IOS_SAFARI_VERSION_UA = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/603.1.23 (KHTML, like Gecko) Version/10.0 Mobile/14E5239e Safari/602.1'; - -// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent#firefox_ua_string -const FIREFOX_UA = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0'; - -// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent#chrome_ua_string -const DESKTOP_CHROME_UA = - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; - -// Source: Me -const DESKTOP_SAFARI_UA = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15'; - -const SUPPORTED_USER_AGENTS = [ - REDUCED_UNSUPPORTED_ANDROID_VERSION_UA, - SUPPORTED_ANDROID_VERSION_CHROME_UA, - SUPPORTED_IOS_CHROME_VERSION_UA, - SUPPORTED_IOS_SAFARI_VERSION_UA, -]; - -const UNSUPPORTED_USER_AGENTS = [ - UNSUPPORTED_ANDROID_VERSION_UA, - SUPPORTED_ANDROID_VERSION_FIREFOX_UA, - UNSUPPORTED_IOS_VERSION_CHROME_UA, - UNSUPPORTED_IOS_VERSION_SAFARI_UA, - FIREFOX_UA, - DESKTOP_CHROME_UA, - DESKTOP_SAFARI_UA, -]; - -describe('isWebauthnPasskeySupported', () => { - const defineProperty = useDefineProperty(); - - UNSUPPORTED_USER_AGENTS.forEach((userAgent) => { - context(userAgent, () => { - beforeEach(() => { - defineProperty(navigator, 'userAgent', { - configurable: true, - value: userAgent, - }); - }); - - it('resolves to false', () => { - expect(isWebauthnPasskeySupported()).to.equal(false); - }); - }); - }); - - SUPPORTED_USER_AGENTS.forEach((userAgent) => { - context(userAgent, () => { - beforeEach(() => { - defineProperty(navigator, 'userAgent', { - configurable: true, - value: userAgent, - }); - }); - - it('resolves to true', () => { - expect(isWebauthnPasskeySupported()).to.equal(true); - }); - }); - }); -}); diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts deleted file mode 100644 index 7ad06f597f0..00000000000 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ /dev/null @@ -1,47 +0,0 @@ -export type IsWebauthnPasskeySupported = () => boolean; - -const MINIMUM_IOS_VERSION = 16; - -const MINIMUM_ANDROID_VERSION = 9; - -const MINIMUM_MACOS_VERSION = 13; - -const MINIMUM_WINDOWS_VERSION = 10 - -function isQualifyingIOSDevice(): boolean { - const match = navigator.userAgent.match(/iPhone; CPU iPhone OS (\d+)_/); - const iOSVersion: null | number = match && Number(match[1]); - return !!iOSVersion && iOSVersion >= MINIMUM_IOS_VERSION; -} - -function isQualifyingAndroidDevice(): boolean { - // Note: Chrome versions applying the "reduced" user agent string will always report a version of - // Android as 10.0.0. - // - // See: https://www.chromium.org/updates/ua-reduction/ - const match = navigator.userAgent.match(/; Android (\d+)/); - const androidVersion: null | number = match && Number(match[1]); - return ( - !!androidVersion && - androidVersion >= MINIMUM_ANDROID_VERSION && - navigator.userAgent.includes(' Chrome/') - ); -} - -function isQualifyingMacOSDesktopDevice(): boolean { - const match = navigator.userAgent.match(/Mac OS X/); - const macOsVersion: null | number = match && Number(match[1]); - return !!macOsVersion && macOsVersion >= MINIMUM_MACOS_VERSION; -} - -function isQualifyingWindowsDesktopDevice(): boolean { - const match = navigator.userAgent.match(/Windows/); - const windowsVersion: null | number = match && Number(match[1]); - return !!windowsVersion && windowsVersion >= MINIMUM_WINDOWS_VERSION; -} - -const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - isQualifyingIOSDevice() || isQualifyingAndroidDevice() - || isQualifyingMacOSDesktopDevice() || isQualifyingWindowsDesktopDevice(); - -export default isWebauthnPasskeySupported; From c208389d5f12d398898cfe80a9daeab68aed3415 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 15 Oct 2024 14:44:46 -0400 Subject: [PATCH 06/62] show_unsupported_passkey_platform_authentication_setup --- app/components/webauthn_input_component.rb | 6 +----- .../webauthn/webauthn-input-element.ts | 5 ++--- .../packs/platform-authenticator-available.ts | 20 ------------------- ...p_webauthn_platform_selection_presenter.rb | 2 -- ...n_webauthn_platform_selection_presenter.rb | 1 - config/application.yml.default | 2 -- lib/identity_config.rb | 1 - 7 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 app/javascript/packs/platform-authenticator-available.ts diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 8c2b952b5f6..1c4624d16d1 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -1,21 +1,18 @@ # frozen_string_literal: true class WebauthnInputComponent < BaseComponent - attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :tag_options + attr_reader :platform, :passkey_supported_only, :tag_options alias_method :platform?, :platform alias_method :passkey_supported_only?, :passkey_supported_only - alias_method :show_unsupported_passkey?, :show_unsupported_passkey def initialize( platform: false, passkey_supported_only: false, - show_unsupported_passkey: false, **tag_options ) @platform = platform @passkey_supported_only = passkey_supported_only - @show_unsupported_passkey = show_unsupported_passkey @tag_options = tag_options end @@ -25,7 +22,6 @@ def call content, **tag_options, **initial_hidden_tag_options, - 'show-unsupported-passkey': show_unsupported_passkey?.presence, ) end diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 954cac625c0..a50e578b14e 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,4 +1,3 @@ -import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; export class WebauthnInputElement extends HTMLElement { @@ -19,9 +18,9 @@ export class WebauthnInputElement extends HTMLElement { return; } - if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) { + if (await isWebauthnPlatformAuthenticatorAvailable()) { this.hidden = false; - } else if (this.showUnsupportedPasskey) { + } else { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } diff --git a/app/javascript/packs/platform-authenticator-available.ts b/app/javascript/packs/platform-authenticator-available.ts deleted file mode 100644 index e77e7195b70..00000000000 --- a/app/javascript/packs/platform-authenticator-available.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index a4fef5d7285..0e8687a3262 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -11,8 +11,6 @@ def render_in(view_context, &block) WebauthnInputComponent.new( platform: true, passkey_supported_only: true, - show_unsupported_passkey: - IdentityConfig.store.show_unsupported_passkey_platform_authentication_setup, ), &block ) diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index 61024f9b02f..03a7162d916 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -11,7 +11,6 @@ def render_in(view_context, &block) WebauthnInputComponent.new( platform: true, passkey_supported_only: false, - show_unsupported_passkey: false, ), &block ) diff --git a/config/application.yml.default b/config/application.yml.default index 83b271c45c8..f60d97423bf 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -339,7 +339,6 @@ session_total_duration_timeout_in_minutes: 720 short_term_phone_otp_max_attempt_window_in_seconds: 10 short_term_phone_otp_max_attempts: 2 show_desktop_ft_unlock_setup_option_percent_tested: 0 -show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false sign_in_recaptcha_percent_tested: 0 sign_in_recaptcha_score_threshold: 0.0 @@ -442,7 +441,6 @@ development: scrypt_cost: 10000$8$1$ secret_key_base: development_secret_key_base session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 - show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_percent_tested: 100 sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 6f9b29d9b92..d3b578d3a1e 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -390,7 +390,6 @@ def self.store config.add(:session_timeout_warning_seconds, type: :integer) config.add(:session_total_duration_timeout_in_minutes, type: :integer) config.add(:show_desktop_ft_unlock_setup_option_percent_tested, type: :integer) - config.add(:show_unsupported_passkey_platform_authentication_setup, type: :boolean) config.add(:show_user_attribute_deprecation_warnings, type: :boolean) config.add(:short_term_phone_otp_max_attempts, type: :integer) config.add(:short_term_phone_otp_max_attempt_window_in_seconds, type: :integer) From 913c635462cccfbfbece8dc3b41c4f91fc12c8a8 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 15 Oct 2024 14:50:59 -0400 Subject: [PATCH 07/62] fix associated test --- spec/components/webauthn_input_component_spec.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb index 2e9f99d9931..eeb0afa7fff 100644 --- a/spec/components/webauthn_input_component_spec.rb +++ b/spec/components/webauthn_input_component_spec.rb @@ -17,10 +17,6 @@ expect(component.passkey_supported_only?).to eq(false) end - it 'exposes boolean alias for show_unsupported_passkey option' do - expect(component.show_unsupported_passkey?).to eq(false) - end - context 'with platform option' do context 'with platform option false' do let(:options) { super().merge(platform: false) } @@ -67,17 +63,6 @@ ) end end - - context 'with show_unsupported_passkey option true' do - let(:options) { super().merge(show_unsupported_passkey: true) } - - it 'renders with show-unsupported-passkey attribute' do - expect(rendered).to have_css( - 'lg-webauthn-input[hidden][show-unsupported-passkey]', - visible: false, - ) - end - end end end end From 8c459c07c5ad06ecbe5eba18cf0cb8fcb4bd0ff0 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 15 Oct 2024 16:36:22 -0400 Subject: [PATCH 08/62] changelog: Upcoming Features, desktop f/t unlock, A/B setup for desktop f/t unlock From 87ac4ffd8d630b49f89be8613964e6aab68de551 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 16 Oct 2024 09:30:04 -0400 Subject: [PATCH 09/62] fix js test --- .../webauthn/webauthn-input-element.spec.ts | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index afd363cc143..36392fa8e73 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,22 +1,15 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; -import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; describe('WebauthnInputElement', () => { - const isWebauthnPasskeySupported = sinon.stub< - Parameters, - ReturnType - >(); - const isWebauthnPlatformAvailable = sinon.stub< Parameters, ReturnType >(); before(async () => { - quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); quibble('./is-webauthn-platform-authenticator-available', isWebauthnPlatformAvailable); await import('./webauthn-input-element'); }); @@ -25,42 +18,39 @@ describe('WebauthnInputElement', () => { quibble.reset(); }); - context('device does not support passkey', () => { - context('unsupported passkey not shown', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(false); - isWebauthnPlatformAvailable.resolves(false); - document.body.innerHTML = ``; - }); + // context('device does not support passkey', () => { + // context('unsupported passkey not shown', () => { + // beforeEach(() => { + // isWebauthnPlatformAvailable.resolves(false); + // document.body.innerHTML = ``; + // }); - it('stays hidden', () => { - const element = document.querySelector('lg-webauthn-input')!; + // it('stays hidden', () => { + // const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.true(); - }); - }); + // expect(element.hidden).to.be.true(); + // }); + // }); - context('unsupported passkey shown', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(false); - isWebauthnPlatformAvailable.resolves(false); - document.body.innerHTML = ``; - }); + // context('unsupported passkey shown', () => { + // beforeEach(() => { + // isWebauthnPlatformAvailable.resolves(false); + // document.body.innerHTML = ``; + // }); - it('becomes visible, with modifier class', () => { - const element = document.querySelector('lg-webauthn-input')!; + // it('becomes visible, with modifier class', () => { + // const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); - expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); - }); - }); - }); + // expect(element.hidden).to.be.false(); + // expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); + // }); + // }); + // }); context('device supports passkey', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { isWebauthnPlatformAvailable.resolves(false); - isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); @@ -73,7 +63,6 @@ describe('WebauthnInputElement', () => { context('publickeycredential input is shown', () => { beforeEach(() => { - isWebauthnPasskeySupported.returns(true); isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; }); From 8adbf86b53e75c53eccafa445696b9d74b192e3f Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 16 Oct 2024 14:32:42 -0400 Subject: [PATCH 10/62] remove `deskton_ab_bucket?`; remove device does not support passkey tests --- app/components/webauthn_input_component.scss | 3 -- .../users/webauthn_setup_controller.rb | 2 -- .../webauthn/webauthn-input-element.spec.ts | 29 ------------------- .../webauthn/webauthn-input-element.ts | 1 - 4 files changed, 35 deletions(-) delete mode 100644 app/components/webauthn_input_component.scss diff --git a/app/components/webauthn_input_component.scss b/app/components/webauthn_input_component.scss deleted file mode 100644 index b6392df9874..00000000000 --- a/app/components/webauthn_input_component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.webauthn-input--unsupported-passkey .usa-checkbox__label { - background: rgb(255 0 0 / 10%); -} diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 0a576d624ac..c90385cfefc 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -61,14 +61,12 @@ def confirm ) result = form.submit(confirm_params) @platform_authenticator = form.platform_authenticator? - @desktop_ab_test_bucket = form.desktop_ab_test_bucket? @presenter = WebauthnSetupPresenter.new( current_user: current_user, user_fully_authenticated: user_fully_authenticated?, user_opted_remember_device_cookie: user_opted_remember_device_cookie, remember_device_default: remember_device_default, platform_authenticator: @platform_authenticator, - desktop_ab_test_bucket: @desktop_ab_test_bucket, url_options:, ) properties = result.to_h.merge(analytics_properties) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 36392fa8e73..0098707726d 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -18,35 +18,6 @@ describe('WebauthnInputElement', () => { quibble.reset(); }); - // context('device does not support passkey', () => { - // context('unsupported passkey not shown', () => { - // beforeEach(() => { - // isWebauthnPlatformAvailable.resolves(false); - // document.body.innerHTML = ``; - // }); - - // it('stays hidden', () => { - // const element = document.querySelector('lg-webauthn-input')!; - - // expect(element.hidden).to.be.true(); - // }); - // }); - - // context('unsupported passkey shown', () => { - // beforeEach(() => { - // isWebauthnPlatformAvailable.resolves(false); - // document.body.innerHTML = ``; - // }); - - // it('becomes visible, with modifier class', () => { - // const element = document.querySelector('lg-webauthn-input')!; - - // expect(element.hidden).to.be.false(); - // expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); - // }); - // }); - // }); - context('device supports passkey', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index a50e578b14e..32a47dd4294 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -22,7 +22,6 @@ export class WebauthnInputElement extends HTMLElement { this.hidden = false; } else { this.hidden = false; - this.classList.add('webauthn-input--unsupported-passkey'); } } } From ca95f08e744c076690aa9e9bbe7d76e654f260fd Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 21 Oct 2024 13:10:51 -0400 Subject: [PATCH 11/62] note to self --- .../packages/webauthn/webauthn-input-element.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 32a47dd4294..cb04aba815a 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -5,23 +5,12 @@ export class WebauthnInputElement extends HTMLElement { this.toggleVisibleIfPasskeySupported(); } - get isPlatform(): boolean { - return this.hasAttribute('platform'); - } - - get showUnsupportedPasskey(): boolean { - return this.hasAttribute('show-unsupported-passkey'); - } - async toggleVisibleIfPasskeySupported() { - if (!this.hasAttribute('hidden')) { - return; - } - if (await isWebauthnPlatformAuthenticatorAvailable()) { + // if user is in the A/B test bucket, show. else, do not show this.hidden = false; } else { - this.hidden = false; + this.hidden = true; } } } From b4f03ef91962e38cde87525bf65388ea64a928e7 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 23 Oct 2024 12:40:34 -0400 Subject: [PATCH 12/62] restore `isWebauthnPaskeySupported` --- app/javascript/packages/webauthn/index.ts | 2 +- .../is-webauthn-passkey-supported.spec.ts | 97 +++++++++++++++++++ .../webauthn/is-webauthn-passkey-supported.ts | 47 +++++++++ .../webauthn/webauthn-input-element.ts | 4 +- 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts create mode 100644 app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index 31f64399acc..a21ec105f22 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -3,7 +3,7 @@ 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/packages/webauthn/is-webauthn-passkey-supported.spec.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts new file mode 100644 index 00000000000..53342b817e3 --- /dev/null +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.spec.ts @@ -0,0 +1,97 @@ +import { useDefineProperty } from '@18f/identity-test-helpers'; +import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; + +// Source (Adapted): https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state +const UNSUPPORTED_ANDROID_VERSION_UA = + 'Mozilla/5.0 (Linux; Android 8; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36'; + +// Source: https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state +const REDUCED_UNSUPPORTED_ANDROID_VERSION_UA = + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36'; + +// Source: https://www.chromium.org/updates/ua-reduction/#sample-ua-strings-final-reduced-state +const SUPPORTED_ANDROID_VERSION_CHROME_UA = + 'Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36'; + +// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox#mobile_and_tablet_indicators +const SUPPORTED_ANDROID_VERSION_FIREFOX_UA = + 'Mozilla/5.0 (Android 9; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'; + +// Source: https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md +const UNSUPPORTED_IOS_VERSION_CHROME_UA = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; + +// Source: https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md +const UNSUPPORTED_IOS_VERSION_SAFARI_UA = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/603.1.23 (KHTML, like Gecko) Version/10.0 Mobile/14E5239e Safari/602.1'; + +// Source (Adapted): https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md +const SUPPORTED_IOS_CHROME_VERSION_UA = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; + +// Source (Adapted): https://chromium.googlesource.com/chromium/src/+/master/docs/ios/user_agent.md +const SUPPORTED_IOS_SAFARI_VERSION_UA = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/603.1.23 (KHTML, like Gecko) Version/10.0 Mobile/14E5239e Safari/602.1'; + +// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent#firefox_ua_string +const FIREFOX_UA = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0'; + +// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent#chrome_ua_string +const DESKTOP_CHROME_UA = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'; + +// Source: Me +const DESKTOP_SAFARI_UA = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15'; + +const SUPPORTED_USER_AGENTS = [ + REDUCED_UNSUPPORTED_ANDROID_VERSION_UA, + SUPPORTED_ANDROID_VERSION_CHROME_UA, + SUPPORTED_IOS_CHROME_VERSION_UA, + SUPPORTED_IOS_SAFARI_VERSION_UA, +]; + +const UNSUPPORTED_USER_AGENTS = [ + UNSUPPORTED_ANDROID_VERSION_UA, + SUPPORTED_ANDROID_VERSION_FIREFOX_UA, + UNSUPPORTED_IOS_VERSION_CHROME_UA, + UNSUPPORTED_IOS_VERSION_SAFARI_UA, + FIREFOX_UA, + DESKTOP_CHROME_UA, + DESKTOP_SAFARI_UA, +]; + +describe('isWebauthnPasskeySupported', () => { + const defineProperty = useDefineProperty(); + + UNSUPPORTED_USER_AGENTS.forEach((userAgent) => { + context(userAgent, () => { + beforeEach(() => { + defineProperty(navigator, 'userAgent', { + configurable: true, + value: userAgent, + }); + }); + + it('resolves to false', () => { + expect(isWebauthnPasskeySupported()).to.equal(false); + }); + }); + }); + + SUPPORTED_USER_AGENTS.forEach((userAgent) => { + context(userAgent, () => { + beforeEach(() => { + defineProperty(navigator, 'userAgent', { + configurable: true, + value: userAgent, + }); + }); + + it('resolves to true', () => { + expect(isWebauthnPasskeySupported()).to.equal(true); + }); + }); + }); +}); diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts new file mode 100644 index 00000000000..7ad06f597f0 --- /dev/null +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -0,0 +1,47 @@ +export type IsWebauthnPasskeySupported = () => boolean; + +const MINIMUM_IOS_VERSION = 16; + +const MINIMUM_ANDROID_VERSION = 9; + +const MINIMUM_MACOS_VERSION = 13; + +const MINIMUM_WINDOWS_VERSION = 10 + +function isQualifyingIOSDevice(): boolean { + const match = navigator.userAgent.match(/iPhone; CPU iPhone OS (\d+)_/); + const iOSVersion: null | number = match && Number(match[1]); + return !!iOSVersion && iOSVersion >= MINIMUM_IOS_VERSION; +} + +function isQualifyingAndroidDevice(): boolean { + // Note: Chrome versions applying the "reduced" user agent string will always report a version of + // Android as 10.0.0. + // + // See: https://www.chromium.org/updates/ua-reduction/ + const match = navigator.userAgent.match(/; Android (\d+)/); + const androidVersion: null | number = match && Number(match[1]); + return ( + !!androidVersion && + androidVersion >= MINIMUM_ANDROID_VERSION && + navigator.userAgent.includes(' Chrome/') + ); +} + +function isQualifyingMacOSDesktopDevice(): boolean { + const match = navigator.userAgent.match(/Mac OS X/); + const macOsVersion: null | number = match && Number(match[1]); + return !!macOsVersion && macOsVersion >= MINIMUM_MACOS_VERSION; +} + +function isQualifyingWindowsDesktopDevice(): boolean { + const match = navigator.userAgent.match(/Windows/); + const windowsVersion: null | number = match && Number(match[1]); + return !!windowsVersion && windowsVersion >= MINIMUM_WINDOWS_VERSION; +} + +const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => + isQualifyingIOSDevice() || isQualifyingAndroidDevice() + || isQualifyingMacOSDesktopDevice() || isQualifyingWindowsDesktopDevice(); + +export default isWebauthnPasskeySupported; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index cb04aba815a..24ce607443f 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,4 +1,5 @@ import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; +import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { @@ -6,8 +7,7 @@ export class WebauthnInputElement extends HTMLElement { } async toggleVisibleIfPasskeySupported() { - if (await isWebauthnPlatformAuthenticatorAvailable()) { - // if user is in the A/B test bucket, show. else, do not show + if (isWebauthnPasskeySupported() && await isWebauthnPlatformAuthenticatorAvailable()) { this.hidden = false; } else { this.hidden = true; From 5097d064bc925695ac2d2133bda6d5655b0c8d7d Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 23 Oct 2024 13:58:53 -0400 Subject: [PATCH 13/62] remove desktop qualifying fns, change logic for supported and available devices --- .../webauthn/is-webauthn-passkey-supported.ts | 15 +-------------- .../packages/webauthn/webauthn-input-element.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index 7ad06f597f0..5348b22f86d 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -28,20 +28,7 @@ function isQualifyingAndroidDevice(): boolean { ); } -function isQualifyingMacOSDesktopDevice(): boolean { - const match = navigator.userAgent.match(/Mac OS X/); - const macOsVersion: null | number = match && Number(match[1]); - return !!macOsVersion && macOsVersion >= MINIMUM_MACOS_VERSION; -} - -function isQualifyingWindowsDesktopDevice(): boolean { - const match = navigator.userAgent.match(/Windows/); - const windowsVersion: null | number = match && Number(match[1]); - return !!windowsVersion && windowsVersion >= MINIMUM_WINDOWS_VERSION; -} - const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - isQualifyingIOSDevice() || isQualifyingAndroidDevice() - || isQualifyingMacOSDesktopDevice() || isQualifyingWindowsDesktopDevice(); + isQualifyingIOSDevice() || isQualifyingAndroidDevice(); export default isWebauthnPasskeySupported; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 24ce607443f..87f6b230769 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -6,8 +6,16 @@ export class WebauthnInputElement extends HTMLElement { this.toggleVisibleIfPasskeySupported(); } + get isOptedInToAbTest(): boolean { + return this.hasAttribute('desktop-ft-ab-test'); + } + + get isPlatform(): boolean { + return this.hasAttribute('platform'); + } + async toggleVisibleIfPasskeySupported() { - if (isWebauthnPasskeySupported() && await isWebauthnPlatformAuthenticatorAvailable()) { + if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else { this.hidden = true; From ec73d0d0c39798609fac5cb49bb4cac3281f4815 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 24 Oct 2024 14:16:56 -0400 Subject: [PATCH 14/62] restore `show_unsupported_passkey` functionality --- app/components/webauthn_input_component.rb | 6 +++- app/components/webauthn_input_component.scss | 3 ++ .../users/webauthn_setup_controller.rb | 2 ++ .../webauthn/webauthn-input-element.spec.ts | 29 +++++++++++++++++++ .../webauthn/webauthn-input-element.ts | 3 +- ...p_webauthn_platform_selection_presenter.rb | 2 ++ ...n_webauthn_platform_selection_presenter.rb | 1 + config/application.yml.default | 2 ++ lib/identity_config.rb | 1 + 9 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 app/components/webauthn_input_component.scss diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 1c4624d16d1..8c2b952b5f6 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -1,18 +1,21 @@ # frozen_string_literal: true class WebauthnInputComponent < BaseComponent - attr_reader :platform, :passkey_supported_only, :tag_options + attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :tag_options alias_method :platform?, :platform alias_method :passkey_supported_only?, :passkey_supported_only + alias_method :show_unsupported_passkey?, :show_unsupported_passkey def initialize( platform: false, passkey_supported_only: false, + show_unsupported_passkey: false, **tag_options ) @platform = platform @passkey_supported_only = passkey_supported_only + @show_unsupported_passkey = show_unsupported_passkey @tag_options = tag_options end @@ -22,6 +25,7 @@ def call content, **tag_options, **initial_hidden_tag_options, + 'show-unsupported-passkey': show_unsupported_passkey?.presence, ) end diff --git a/app/components/webauthn_input_component.scss b/app/components/webauthn_input_component.scss new file mode 100644 index 00000000000..b6392df9874 --- /dev/null +++ b/app/components/webauthn_input_component.scss @@ -0,0 +1,3 @@ +.webauthn-input--unsupported-passkey .usa-checkbox__label { + background: rgb(255 0 0 / 10%); +} diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index c90385cfefc..0a576d624ac 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -61,12 +61,14 @@ def confirm ) result = form.submit(confirm_params) @platform_authenticator = form.platform_authenticator? + @desktop_ab_test_bucket = form.desktop_ab_test_bucket? @presenter = WebauthnSetupPresenter.new( current_user: current_user, user_fully_authenticated: user_fully_authenticated?, user_opted_remember_device_cookie: user_opted_remember_device_cookie, remember_device_default: remember_device_default, platform_authenticator: @platform_authenticator, + desktop_ab_test_bucket: @desktop_ab_test_bucket, url_options:, ) properties = result.to_h.merge(analytics_properties) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 0098707726d..8ec33deb1bf 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -18,6 +18,35 @@ describe('WebauthnInputElement', () => { quibble.reset(); }); + context('device does not support passkey', () => { + context('unsupported passkey not shown', () => { + beforeEach(() => { + isWebauthnPlatformAvailable.resolves(false); + document.body.innerHTML = ``; + }); + + it('stays hidden', () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.true(); + }); + }); + + context('unsupported passkey shown', () => { + beforeEach(() => { + isWebauthnPlatformAvailable.resolves(false); + document.body.innerHTML = ``; + }); + + it('becomes visible, with modifier class', () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.false(); + expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); + }); + }); + }); + context('device supports passkey', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 87f6b230769..2d7388a8404 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -18,7 +18,8 @@ export class WebauthnInputElement extends HTMLElement { if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else { - this.hidden = true; + this.hidden = false; + this.classList.add('webauthn-input--unsupported-passkey'); } } } diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index 0e8687a3262..a4fef5d7285 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -11,6 +11,8 @@ def render_in(view_context, &block) WebauthnInputComponent.new( platform: true, passkey_supported_only: true, + show_unsupported_passkey: + IdentityConfig.store.show_unsupported_passkey_platform_authentication_setup, ), &block ) diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index 03a7162d916..61024f9b02f 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -11,6 +11,7 @@ def render_in(view_context, &block) WebauthnInputComponent.new( platform: true, passkey_supported_only: false, + show_unsupported_passkey: false, ), &block ) diff --git a/config/application.yml.default b/config/application.yml.default index 650cd240abb..b0999951ef0 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -338,6 +338,7 @@ session_total_duration_timeout_in_minutes: 720 short_term_phone_otp_max_attempt_window_in_seconds: 10 short_term_phone_otp_max_attempts: 2 show_desktop_ft_unlock_setup_option_percent_tested: 0 +show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false sign_in_recaptcha_percent_tested: 0 sign_in_recaptcha_score_threshold: 0.0 @@ -441,6 +442,7 @@ development: scrypt_cost: 10000$8$1$ secret_key_base: development_secret_key_base session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 + show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_percent_tested: 100 sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index ed239fa0fbc..6435472304c 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -389,6 +389,7 @@ def self.store config.add(:session_timeout_warning_seconds, type: :integer) config.add(:session_total_duration_timeout_in_minutes, type: :integer) config.add(:show_desktop_ft_unlock_setup_option_percent_tested, type: :integer) + config.add(:show_unsupported_passkey_platform_authentication_setup, type: :boolean) config.add(:show_user_attribute_deprecation_warnings, type: :boolean) config.add(:short_term_phone_otp_max_attempts, type: :integer) config.add(:short_term_phone_otp_max_attempt_window_in_seconds, type: :integer) From 079a41db40560cace811a5ffd34f10796a9a6a18 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 24 Oct 2024 14:31:33 -0400 Subject: [PATCH 15/62] lintfixes --- .../packages/webauthn/is-webauthn-passkey-supported.ts | 4 ---- app/javascript/packages/webauthn/webauthn-input-element.ts | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index 5348b22f86d..b496028322b 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -4,10 +4,6 @@ const MINIMUM_IOS_VERSION = 16; const MINIMUM_ANDROID_VERSION = 9; -const MINIMUM_MACOS_VERSION = 13; - -const MINIMUM_WINDOWS_VERSION = 10 - function isQualifyingIOSDevice(): boolean { const match = navigator.userAgent.match(/iPhone; CPU iPhone OS (\d+)_/); const iOSVersion: null | number = match && Number(match[1]); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 2d7388a8404..457fa2dbf47 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -15,7 +15,10 @@ export class WebauthnInputElement extends HTMLElement { } async toggleVisibleIfPasskeySupported() { - if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { + if ( + (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && + (await isWebauthnPlatformAuthenticatorAvailable()) + ) { this.hidden = false; } else { this.hidden = false; From ceea8ccf0f8f3f7e04028674f7558f09c25c9ec9 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 24 Oct 2024 14:57:41 -0400 Subject: [PATCH 16/62] fix tests --- .../packages/webauthn/webauthn-input-element.spec.ts | 11 +++++++++++ .../packages/webauthn/webauthn-input-element.ts | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 8ec33deb1bf..afd363cc143 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,15 +1,22 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; +import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; describe('WebauthnInputElement', () => { + const isWebauthnPasskeySupported = sinon.stub< + Parameters, + ReturnType + >(); + const isWebauthnPlatformAvailable = sinon.stub< Parameters, ReturnType >(); before(async () => { + quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); quibble('./is-webauthn-platform-authenticator-available', isWebauthnPlatformAvailable); await import('./webauthn-input-element'); }); @@ -21,6 +28,7 @@ describe('WebauthnInputElement', () => { context('device does not support passkey', () => { context('unsupported passkey not shown', () => { beforeEach(() => { + isWebauthnPasskeySupported.returns(false); isWebauthnPlatformAvailable.resolves(false); document.body.innerHTML = ``; }); @@ -34,6 +42,7 @@ describe('WebauthnInputElement', () => { context('unsupported passkey shown', () => { beforeEach(() => { + isWebauthnPasskeySupported.returns(false); isWebauthnPlatformAvailable.resolves(false); document.body.innerHTML = ``; }); @@ -51,6 +60,7 @@ describe('WebauthnInputElement', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { isWebauthnPlatformAvailable.resolves(false); + isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); @@ -63,6 +73,7 @@ describe('WebauthnInputElement', () => { context('publickeycredential input is shown', () => { beforeEach(() => { + isWebauthnPasskeySupported.returns(true); isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 457fa2dbf47..8ab9d0ab0f0 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -14,13 +14,21 @@ export class WebauthnInputElement extends HTMLElement { return this.hasAttribute('platform'); } + get showUnsupportedPasskey(): boolean { + return this.hasAttribute('show-unsupported-passkey'); + } + async toggleVisibleIfPasskeySupported() { + if (!this.hasAttribute('hidden')) { + return; + } + if ( (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable()) ) { this.hidden = false; - } else { + } else if (this.showUnsupportedPasskey) { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } From 7fb13fe657a791b270279c4aeeea4f51c9912624 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 24 Oct 2024 15:56:53 -0400 Subject: [PATCH 17/62] remove `@desktop_ab_test_bucket` --- app/controllers/users/webauthn_setup_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 0a576d624ac..c90385cfefc 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -61,14 +61,12 @@ def confirm ) result = form.submit(confirm_params) @platform_authenticator = form.platform_authenticator? - @desktop_ab_test_bucket = form.desktop_ab_test_bucket? @presenter = WebauthnSetupPresenter.new( current_user: current_user, user_fully_authenticated: user_fully_authenticated?, user_opted_remember_device_cookie: user_opted_remember_device_cookie, remember_device_default: remember_device_default, platform_authenticator: @platform_authenticator, - desktop_ab_test_bucket: @desktop_ab_test_bucket, url_options:, ) properties = result.to_h.merge(analytics_properties) From 024db55ef45eaf6c74be09fa012105dd28c283d6 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 25 Oct 2024 10:18:58 -0400 Subject: [PATCH 18/62] rename to `desktop_ft_unlock_setup_option_percent_tested` --- config/application.yml.default | 3 ++- config/initializers/ab_tests.rb | 2 +- lib/identity_config.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/application.yml.default b/config/application.yml.default index b0999951ef0..c3a0c95f5ca 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -84,6 +84,7 @@ database_worker_jobs_sslmode: 'verify-full' database_worker_jobs_username: '' deleted_user_accounts_report_configs: '[]' deliver_mail_async: false +desktop_ft_unlock_setup_option_percent_tested: 0 development_mailer_deliver_method: letter_opener disable_email_sending: true disable_logout_get_request: true @@ -337,7 +338,6 @@ session_timeout_warning_seconds: 150 session_total_duration_timeout_in_minutes: 720 short_term_phone_otp_max_attempt_window_in_seconds: 10 short_term_phone_otp_max_attempts: 2 -show_desktop_ft_unlock_setup_option_percent_tested: 0 show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false sign_in_recaptcha_percent_tested: 0 @@ -415,6 +415,7 @@ development: compromised_password_randomizer_value: 1 dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers + desktop_ft_unlock_setup_option_percent_tested: 100 doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 80fb8b0f96f..ca4b4ad91a3 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -89,7 +89,7 @@ def self.all 'Multi-Factor Authentication Setup', ].to_set, buckets: { desktop_ft_unlock: - IdentityConfig.store.show_desktop_ft_unlock_setup_option_percent_tested }, + IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested }, ) do |user:, user_session:, **| if user_session&.[](:platform_authenticator_available) == false nil diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 6435472304c..5c1ba876415 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -107,6 +107,7 @@ def self.store config.add(:database_worker_jobs_username, type: :string) config.add(:deleted_user_accounts_report_configs, type: :json) config.add(:deliver_mail_async, type: :boolean) + config.add(:desktop_ft_unlock_setup_option_percent_tested, type: :integer) config.add(:development_mailer_deliver_method, type: :symbol, enum: [:file, :letter_opener]) config.add(:disable_email_sending, type: :boolean) config.add(:disable_logout_get_request, type: :boolean) @@ -388,7 +389,6 @@ def self.store config.add(:session_timeout_in_minutes, type: :integer) config.add(:session_timeout_warning_seconds, type: :integer) config.add(:session_total_duration_timeout_in_minutes, type: :integer) - config.add(:show_desktop_ft_unlock_setup_option_percent_tested, type: :integer) config.add(:show_unsupported_passkey_platform_authentication_setup, type: :boolean) config.add(:show_user_attribute_deprecation_warnings, type: :boolean) config.add(:short_term_phone_otp_max_attempts, type: :integer) From bdf4e11ede233cb8f1f2a00b4bec49382c807350 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 28 Oct 2024 11:07:57 -0400 Subject: [PATCH 19/62] work on specs for A/B test --- config/initializers/ab_tests.rb | 2 +- spec/config/initializers/ab_tests_spec.rb | 56 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index ca4b4ad91a3..ee490102504 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -88,7 +88,7 @@ def self.all 'WebAuthn Setup Visited', 'Multi-Factor Authentication Setup', ].to_set, - buckets: { desktop_ft_unlock: + buckets: { desktop_ft_unlock_setup: IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested }, ) do |user:, user_session:, **| if user_session&.[](:platform_authenticator_available) == false diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index 2e9fef97486..caeeb9c2978 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -219,4 +219,60 @@ end end end + + describe 'DESKTOP_FT_UNLOCK_SETUP' do + let(:user) { nil } + let(:user_session) { {} } + + subject(:bucket) do + AbTests::DESKTOP_FT_UNLOCK_SETUP.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session:, + ) + end + + context 'when A/B test is disabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(0) + reload_ab_tests + end + + context 'when it would otherwise assign a bucket' do + let(:user) { build(:user) } + + it 'does not return a bucket' do + expect(bucket).to be_nil + end + end + end + + context 'when A/B test is enabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(100) + reload_ab_tests + end + + let(:user) { build(:user) } + + it 'returns a bucket' do + expect(bucket).not_to be_nil + end + + context 'when A/B test is disabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested).and_return(0) + reload_ab_tests + end + + it 'does not return a bucket' do + expect(bucket).to be_nil + end + end + end + end end From ef448628281011eeb512ded2f06bbf74c865c235 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 28 Oct 2024 12:52:27 -0400 Subject: [PATCH 20/62] add tag so that functionality to show/hide can be in place --- app/components/webauthn_input_component.rb | 15 ++++++++++++++- .../packages/webauthn/webauthn-input-element.ts | 2 +- ...et_up_webauthn_platform_selection_presenter.rb | 1 + ...gn_in_webauthn_platform_selection_presenter.rb | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 8c2b952b5f6..c09169b4238 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -1,21 +1,25 @@ # frozen_string_literal: true class WebauthnInputComponent < BaseComponent - attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :tag_options + attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :desktop_ft_unlock_option, + :tag_options alias_method :platform?, :platform alias_method :passkey_supported_only?, :passkey_supported_only alias_method :show_unsupported_passkey?, :show_unsupported_passkey + alias_method :desktop_ft_unlock_option?, :desktop_ft_unlock_option def initialize( platform: false, passkey_supported_only: false, show_unsupported_passkey: false, + desktop_ft_unlock_option: false, **tag_options ) @platform = platform @passkey_supported_only = passkey_supported_only @show_unsupported_passkey = show_unsupported_passkey + @desktop_ft_unlock_option = desktop_ft_unlock_option @tag_options = tag_options end @@ -26,6 +30,7 @@ def call **tag_options, **initial_hidden_tag_options, 'show-unsupported-passkey': show_unsupported_passkey?.presence, + 'desktop-ft-unlock-option': show_desktop_ft_unlock_option?, ) end @@ -36,4 +41,12 @@ def initial_hidden_tag_options { class: 'js' } end end + + def show_desktop_ft_unlock_option? + if desktop_ft_unlock_option?.presence && I18n.locale == :en + true + else + false + end + end end diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 8ab9d0ab0f0..a7e9759b6c4 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -7,7 +7,7 @@ export class WebauthnInputElement extends HTMLElement { } get isOptedInToAbTest(): boolean { - return this.hasAttribute('desktop-ft-ab-test'); + return this.hasAttribute('desktop-ft-unlock-option'); } get isPlatform(): boolean { diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index a4fef5d7285..5a26c6d1f94 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -13,6 +13,7 @@ def render_in(view_context, &block) passkey_supported_only: true, show_unsupported_passkey: IdentityConfig.store.show_unsupported_passkey_platform_authentication_setup, + desktop_ft_unlock_option: true, ), &block ) diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index 61024f9b02f..8066a01c3e2 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -12,6 +12,7 @@ def render_in(view_context, &block) platform: true, passkey_supported_only: false, show_unsupported_passkey: false, + desktop_ft_unlock_option: false, ), &block ) From 6dfd8424cf558b663fc2b2a4be8c75699ef2b333 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 28 Oct 2024 15:49:02 -0400 Subject: [PATCH 21/62] toggle show based on english language --- app/components/webauthn_input_component.rb | 2 +- .../packages/webauthn/webauthn-input-element.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index c09169b4238..6fd86ad739e 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -30,7 +30,7 @@ def call **tag_options, **initial_hidden_tag_options, 'show-unsupported-passkey': show_unsupported_passkey?.presence, - 'desktop-ft-unlock-option': show_desktop_ft_unlock_option?, + 'desktop-ft-unlock-option': show_desktop_ft_unlock_option?.presence, ) end diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index a7e9759b6c4..3df2269b8f5 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -4,6 +4,7 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { this.toggleVisibleIfPasskeySupported(); + this.toggleAbtestOption(); } get isOptedInToAbTest(): boolean { @@ -23,16 +24,21 @@ export class WebauthnInputElement extends HTMLElement { return; } - if ( - (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && - (await isWebauthnPlatformAuthenticatorAvailable()) - ) { + if (isWebauthnPasskeySupported() &&(await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } } + + async toggleAbtestOption() { + if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { + this.hidden = false; + } else if (!this.isOptedInToAbTest){ + this.hidden = true; + } + } } declare global { From c0f3bdf89256d5f7a0830b07dca55bdfada605dc Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 30 Oct 2024 14:18:01 -0400 Subject: [PATCH 22/62] restore conditional to show based on A/B enablement --- .../packages/webauthn/webauthn-input-element.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 3df2269b8f5..80b10198d4c 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -4,7 +4,6 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { this.toggleVisibleIfPasskeySupported(); - this.toggleAbtestOption(); } get isOptedInToAbTest(): boolean { @@ -24,21 +23,13 @@ export class WebauthnInputElement extends HTMLElement { return; } - if (isWebauthnPasskeySupported() &&(await isWebauthnPlatformAuthenticatorAvailable())) { + if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } } - - async toggleAbtestOption() { - if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { - this.hidden = false; - } else if (!this.isOptedInToAbTest){ - this.hidden = true; - } - } } declare global { From 41433036bfa29fc1bfbd235a8e213a122b6a351d Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 31 Oct 2024 09:12:58 -0400 Subject: [PATCH 23/62] add javascript test --- .../webauthn/webauthn-input-element.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index afd363cc143..65b3928d24c 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -85,4 +85,34 @@ describe('WebauthnInputElement', () => { }); }); }); + + context('Desktop F/T unlock A/B test', () => { + context('desktop F/T unlock setup enabled', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.resolves(true); + document.body.innerHTML = ``; + }); + + it('becomes visible', async () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.false(); + }); + }); + + context('desktop F/T unlock setup disabled', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(true); + isWebauthnPlatformAvailable.resolves(false); + document.body.innerHTML = ``; + }); + + it('is hidden', async () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.false(); + }); + }); + }); }); From a0fddd9709e17a5737ef28a9d9f513936e7f3bbd Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 31 Oct 2024 09:28:06 -0400 Subject: [PATCH 24/62] lintfix --- app/components/webauthn_input_component.rb | 4 ++-- spec/config/initializers/ab_tests_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 6fd86ad739e..10bc9f22070 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class WebauthnInputComponent < BaseComponent - attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :desktop_ft_unlock_option, - :tag_options + attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, + :desktop_ft_unlock_option, :tag_options alias_method :platform?, :platform alias_method :passkey_supported_only?, :passkey_supported_only diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index caeeb9c2978..c2fd07b76bf 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -265,7 +265,8 @@ context 'when A/B test is disabled' do before do - allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested).and_return(0) + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(0) reload_ab_tests end From 7308178db17ce40a0acbedb1d802535dd9d8acad Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 31 Oct 2024 10:23:52 -0400 Subject: [PATCH 25/62] changelog: Upcoming Features, A/B test, create A/B test for desktop F/T unlock setup From c144b2cbc70191d052e8d030c6a006e349f5df27 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 31 Oct 2024 11:57:22 -0400 Subject: [PATCH 26/62] fix setup for desktop f/t unlock test --- config/initializers/ab_tests.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index ee490102504..06525614c36 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -90,11 +90,5 @@ def self.all ].to_set, buckets: { desktop_ft_unlock_setup: IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested }, - ) do |user:, user_session:, **| - if user_session&.[](:platform_authenticator_available) == false - nil - else - user.uuid - end - end.freeze + ).freeze end From b979abb4d9f8ffcd62d93139d441df98cd27fc99 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 31 Oct 2024 12:05:13 -0400 Subject: [PATCH 27/62] lintfixes --- .../packages/webauthn/webauthn-input-element.spec.ts | 8 ++++---- .../packages/webauthn/webauthn-input-element.ts | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 65b3928d24c..cadc8a43fde 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -94,7 +94,7 @@ describe('WebauthnInputElement', () => { document.body.innerHTML = ``; }); - it('becomes visible', async () => { + it('becomes visible', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); @@ -103,12 +103,12 @@ describe('WebauthnInputElement', () => { context('desktop F/T unlock setup disabled', () => { beforeEach(() => { - isWebauthnPasskeySupported.returns(true); - isWebauthnPlatformAvailable.resolves(false); + isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; }); - it('is hidden', async () => { + it('is hidden',() => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 80b10198d4c..a7e9759b6c4 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -23,7 +23,10 @@ export class WebauthnInputElement extends HTMLElement { return; } - if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { + if ( + (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && + (await isWebauthnPlatformAuthenticatorAvailable()) + ) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From eeb87882a6b5ed8c0cddd06845a876eb36be6bd7 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 4 Nov 2024 14:50:01 -0500 Subject: [PATCH 28/62] track event when user is in a/b test but would not show otherwise --- .../packages/webauthn/webauthn-input-element.ts | 12 ++++++++---- app/services/analytics_events.rb | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index a7e9759b6c4..bb8642b464f 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,5 +1,6 @@ import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; +import { trackEvent } from '@18f/identity-analytics'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { @@ -19,19 +20,22 @@ export class WebauthnInputElement extends HTMLElement { } async toggleVisibleIfPasskeySupported() { + const webauthnPlatformAvailable = await isWebauthnPlatformAuthenticatorAvailable(); + if (!this.hasAttribute('hidden')) { return; } - if ( - (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && - (await isWebauthnPlatformAuthenticatorAvailable()) - ) { + if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && webauthnPlatformAvailable) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } + + if(this.isOptedInToAbTest && webauthnPlatformAvailable) { + trackEvent('desktop_ab_test_option_shown') + } } } diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 25ed4fb4c79..aa0e219ac10 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -411,6 +411,11 @@ def create_new_device_alert_job_emails_sent(count:, **extra) track_event(:create_new_device_alert_job_emails_sent, count:, **extra) end + # For those in the desktop F/T unlock bucket, tracks when the user sees the F/T unlock option + def desktop_ab_test_option_shown + track_event(:desktop_ab_test_option_shown) + end + # @param [String] message the warning # @param [Array] unknown_alerts Names of alerts not recognized by our code # @param [Hash] response_info Response payload From cb8d18cdfc76eb43d4aa2149a866c0193487a6ec Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 6 Nov 2024 15:33:15 -0500 Subject: [PATCH 29/62] WIP: show/hide based on bucket --- .../users/two_factor_authentication_setup_controller.rb | 1 + .../set_up_selection_presenter.rb | 8 +++++--- app/presenters/two_factor_options_presenter.rb | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 0ef866928b7..df422094b5e 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -65,6 +65,7 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, + desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) ) end diff --git a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb index 798cd289c67..a518e4bd67e 100644 --- a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb @@ -4,7 +4,7 @@ module TwoFactorAuthentication class SetUpSelectionPresenter include ActionView::Helpers::TranslationHelper - attr_reader :user, :piv_cac_required, :phishing_resistant_required, :user_agent + attr_reader :user, :piv_cac_required, :phishing_resistant_required, :user_agent, :desktop_ft_ab_test alias_method :piv_cac_required?, :piv_cac_required alias_method :phishing_resistant_required?, :phishing_resistant_required @@ -12,12 +12,14 @@ def initialize( user:, piv_cac_required: false, phishing_resistant_required: false, - user_agent: nil + user_agent: nil, + desktop_ft_ab_test: nil ) @user = user @piv_cac_required = piv_cac_required @phishing_resistant_required = phishing_resistant_required - @user_agent = user_agent + @user_agent = user_agent, + @desktop_ft_ab_test = desktop_ft_ab_test end def render_in(view_context, &block) diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 3477afbd009..02b8d9c4225 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -8,7 +8,8 @@ class TwoFactorOptionsPresenter :return_to_sp_cancel_path, :phishing_resistant_required, :piv_cac_required, - :user_agent + :user_agent, + :desktop_ft_ab_test delegate :two_factor_enabled?, to: :mfa_policy def initialize( @@ -18,7 +19,8 @@ def initialize( piv_cac_required: false, show_skip_additional_mfa_link: true, after_mfa_setup_path: nil, - return_to_sp_cancel_path: nil + return_to_sp_cancel_path: nil, + desktop_ft_ab_test: nil ) @user_agent = user_agent @user = user @@ -27,6 +29,7 @@ def initialize( @show_skip_additional_mfa_link = show_skip_additional_mfa_link @after_mfa_setup_path = after_mfa_setup_path @return_to_sp_cancel_path = return_to_sp_cancel_path + @desktop_ft_ab_test = desktop_ft_ab_test end def options @@ -46,7 +49,7 @@ def all_options_sorted user:, piv_cac_required: piv_cac_required?, phishing_resistant_required: phishing_resistant_only?, - user_agent:, + user_agent: ) end. partition(&:recommended?). From e33c39e9909063901eb71db48bfc61520f0e9a5f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 6 Nov 2024 15:45:16 -0500 Subject: [PATCH 30/62] Add component tests for desktop-ft-unlock-option --- .../webauthn_input_component_spec.rb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb index eeb0afa7fff..13f2f7d3c00 100644 --- a/spec/components/webauthn_input_component_spec.rb +++ b/spec/components/webauthn_input_component_spec.rb @@ -17,6 +17,28 @@ expect(component.passkey_supported_only?).to eq(false) end + it 'does not render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input:not([desktop-ft-unlock-option="false"])') + end + + context 'with desktop_ft_unlock_option' do + let(:options) { super().merge(desktop_ft_unlock_option: true) } + + it 'does render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input[desktop-ft-unlock-option="true"]') + end + + context 'in a locale other than english' do + before do + I18n.locale = I18n.available_locales.sample + end + + it 'does not render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input:not([desktop-ft-unlock-option="false"])') + end + end + end + context 'with platform option' do context 'with platform option false' do let(:options) { super().merge(platform: false) } From 264c5596187a04d10ba39c84c262f35d3521e0c6 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 6 Nov 2024 15:52:14 -0500 Subject: [PATCH 31/62] Add controller specs for presenter assigns ab test value --- ...or_authentication_setup_controller_spec.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 0a89f10fbd7..cf0db8a6371 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -4,6 +4,8 @@ describe 'GET index' do let(:user) { create(:user) } + subject(:response) { get :index } + before do stub_sign_in_before_2fa(user) if user stub_analytics @@ -19,6 +21,12 @@ ) end + it 'initializes presenter with blank ab test bucket value' do + response + + expect(assigns(:presenter).desktop_ft_ab_test).to be_nil + end + context 'with user having gov or mil email' do let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } let(:user) do @@ -101,6 +109,18 @@ expect(response).to redirect_to(user_two_factor_authentication_url) end end + + context 'with user opted in to desktop ft unlock setup ab test' do + before do + allow(controller).to receive(:ab_test_bucket).and_return(:desktop_ft_unlock_setup) + end + + it 'initializes presenter with ab test bucket value' do + response + + expect(assigns(:presenter).desktop_ft_ab_test).to eq(:desktop_ft_unlock_setup) + end + end end describe '#create' do From 3e42ff7458bf4432be17978a293733875eabfcec Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 6 Nov 2024 15:59:41 -0500 Subject: [PATCH 32/62] Fix syntax error on assignment --- .../set_up_selection_presenter.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb index a518e4bd67e..7942bcfc260 100644 --- a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb @@ -4,7 +4,11 @@ module TwoFactorAuthentication class SetUpSelectionPresenter include ActionView::Helpers::TranslationHelper - attr_reader :user, :piv_cac_required, :phishing_resistant_required, :user_agent, :desktop_ft_ab_test + attr_reader :user, + :piv_cac_required, + :phishing_resistant_required, + :user_agent, + :desktop_ft_ab_test alias_method :piv_cac_required?, :piv_cac_required alias_method :phishing_resistant_required?, :phishing_resistant_required @@ -18,7 +22,7 @@ def initialize( @user = user @piv_cac_required = piv_cac_required @phishing_resistant_required = phishing_resistant_required - @user_agent = user_agent, + @user_agent = user_agent @desktop_ft_ab_test = desktop_ft_ab_test end From 8086b69bea5f4f3aec61775c380b55aa891b0785 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 6 Nov 2024 15:59:54 -0500 Subject: [PATCH 33/62] Add feature test for A/B test setup on desktop --- spec/features/webauthn/hidden_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 57b52cbb92b..bb5f9240ad4 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -3,6 +3,7 @@ RSpec.describe 'webauthn hide' do include JavascriptDriverHelper include WebAuthnHelper + include AbTestsHelper describe 'security key' do let(:option_id) { 'two_factor_options_form_selection_webauthn' } @@ -59,6 +60,25 @@ expect(webauthn_option_hidden?).to eq(true) end + context 'when in ab test for desktop setup' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(100) + reload_ab_tests + end + + after do + reload_ab_tests + end + + it 'displays the authenticator option' do + sign_up_and_set_password + simulate_platform_authenticator_available + + expect(webauthn_option_hidden?).to eq(false) + end + end + context 'with supported browser and platform authenticator available', driver: :headless_chrome_mobile do it 'displays the authenticator option' do From 7139ad25c779df6ac57e36ce4b97a9fe995c2ecf Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 6 Nov 2024 17:05:57 -0500 Subject: [PATCH 34/62] fix js code and tests --- .../webauthn/webauthn-input-element.spec.ts | 28 ++++++++++++++----- .../webauthn/webauthn-input-element.ts | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index cadc8a43fde..1fa0844923e 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,6 +1,8 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; +import { useSandbox } from '@18f/identity-test-helpers'; +import * as analytics from '@18f/identity-analytics'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; @@ -50,8 +52,8 @@ describe('WebauthnInputElement', () => { it('becomes visible, with modifier class', () => { const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); - expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); + expect(element.hidden).to.be.true(); + expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.false(); }); }); }); @@ -88,31 +90,43 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { + const sandbox = useSandbox() beforeEach(() => { - isWebauthnPasskeySupported.returns(false); isWebauthnPlatformAvailable.resolves(true); + sandbox.stub(analytics, 'trackEvent'); document.body.innerHTML = ``; }); it('becomes visible', () => { const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); }); }); context('desktop F/T unlock setup disabled', () => { beforeEach(() => { - isWebauthnPasskeySupported.returns(false); - isWebauthnPlatformAvailable.resolves(true); + isWebauthnPlatformAvailable.resolves(false); document.body.innerHTML = ``; }); - it('is hidden',() => { + it('is hidden when passkeys are supported',() => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); }); }); + + context('when platform authenticator option is unavailable', () => { + const sandbox = useSandbox() + beforeEach(() => { + isWebauthnPlatformAvailable.resolves(false); + document.body.innerHTML = ``; + sandbox.stub(analytics, 'trackEvent'); + }); + + it('does not log the event', () => { + expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); + }); + }) }); }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index bb8642b464f..a3d1055bec7 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -33,7 +33,7 @@ export class WebauthnInputElement extends HTMLElement { this.classList.add('webauthn-input--unsupported-passkey'); } - if(this.isOptedInToAbTest && webauthnPlatformAvailable) { + if(this.isOptedInToAbTest && !webauthnPlatformAvailable) { trackEvent('desktop_ab_test_option_shown') } } From 47b43bb026d79ca11f66655ffdeccadebc9e2037 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 7 Nov 2024 09:38:00 -0500 Subject: [PATCH 35/62] fix tests and associated logic --- .../packages/webauthn/webauthn-input-element.spec.ts | 12 ++++++++---- .../packages/webauthn/webauthn-input-element.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 1fa0844923e..3c804f39b27 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -101,6 +101,10 @@ describe('WebauthnInputElement', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); }); + + it('does not log the event', () => { + expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown') + }) }); context('desktop F/T unlock setup disabled', () => { @@ -116,15 +120,15 @@ describe('WebauthnInputElement', () => { }); }); - context('when platform authenticator option is unavailable', () => { + context('when the setup option would be hidden', () => { const sandbox = useSandbox() beforeEach(() => { - isWebauthnPlatformAvailable.resolves(false); - document.body.innerHTML = ``; + isWebauthnPlatformAvailable.resolves(true); + document.body.innerHTML = ``; sandbox.stub(analytics, 'trackEvent'); }); - it('does not log the event', () => { + it('logs the event', () => { expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); }); }) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index a3d1055bec7..af71c0edd0f 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -33,7 +33,7 @@ export class WebauthnInputElement extends HTMLElement { this.classList.add('webauthn-input--unsupported-passkey'); } - if(this.isOptedInToAbTest && !webauthnPlatformAvailable) { + if(this.isOptedInToAbTest && this.hasAttribute('hidden')) { trackEvent('desktop_ab_test_option_shown') } } From bc48f5b0df6b8f36bb616041ef33095492e4dc73 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 7 Nov 2024 16:22:36 -0500 Subject: [PATCH 36/62] add desktop ft unlock capability on login options --- .../two_factor_authentication/options_controller.rb | 1 + app/presenters/two_factor_login_options_presenter.rb | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb index 9d74854fad7..331f0f7dcaa 100644 --- a/app/controllers/two_factor_authentication/options_controller.rb +++ b/app/controllers/two_factor_authentication/options_controller.rb @@ -55,6 +55,7 @@ def two_factor_options_presenter 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?, + desktop_ab_test_option: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP), ) end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index bcc13ba0747..3d0c0d31b45 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -22,7 +22,8 @@ def initialize( service_provider:, phishing_resistant_required:, piv_cac_required:, - add_piv_cac_after_2fa: + add_piv_cac_after_2fa:, + desktop_ab_test_option: nil ) @user = user @view = view @@ -31,6 +32,7 @@ def initialize( @phishing_resistant_required = phishing_resistant_required @piv_cac_required = piv_cac_required @add_piv_cac_after_2fa = add_piv_cac_after_2fa + @desktop_ab_test_option = desktop_ab_test_option end def title From 2303a72fb01c062a814bffd527bc6641c8293cac Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 09:21:18 -0500 Subject: [PATCH 37/62] set logic for `desktop-ft-unlock-option` class on sign in for test --- .../sign_in_webauthn_platform_selection_presenter.rb | 2 +- app/presenters/two_factor_login_options_presenter.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index 8066a01c3e2..4ea1c2e473d 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -12,7 +12,7 @@ def render_in(view_context, &block) platform: true, passkey_supported_only: false, show_unsupported_passkey: false, - desktop_ft_unlock_option: false, + desktop_ft_unlock_option: true, ), &block ) diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index 3d0c0d31b45..a59d8e874f3 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -8,7 +8,8 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresent :reauthentication_context, :phishing_resistant_required, :piv_cac_required, - :add_piv_cac_after_2fa + :add_piv_cac_after_2fa, + :desktop_ab_test_option alias_method :reauthentication_context?, :reauthentication_context alias_method :phishing_resistant_required?, :phishing_resistant_required From 8a1157893ba6e9e9f125683cfcff5c3313cb5522 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 11:04:44 -0500 Subject: [PATCH 38/62] show/hide based on value --- app/components/webauthn_input_component.rb | 4 ++-- .../users/two_factor_authentication_setup_controller.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 10bc9f22070..d208baf9d40 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -35,7 +35,7 @@ def call end def initial_hidden_tag_options - if platform? && passkey_supported_only? + if platform? && passkey_supported_only? || !desktop_ft_unlock_option? && n { hidden: true } else { class: 'js' } @@ -43,7 +43,7 @@ def initial_hidden_tag_options end def show_desktop_ft_unlock_option? - if desktop_ft_unlock_option?.presence && I18n.locale == :en + if desktop_ft_unlock_option? && I18n.locale == :en true else false diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index df422094b5e..17714a43e14 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -65,7 +65,7 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, - desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) + desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP), ) end From c589dab8cfe0e51f0f23af62bc6b40529fd8f2d9 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 11:23:55 -0500 Subject: [PATCH 39/62] fix error on line --- app/components/webauthn_input_component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index d208baf9d40..1eaec1e5cc3 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -35,7 +35,7 @@ def call end def initial_hidden_tag_options - if platform? && passkey_supported_only? || !desktop_ft_unlock_option? && n + if platform? && passkey_supported_only? || !desktop_ft_unlock_option? { hidden: true } else { class: 'js' } From 48486969b2eca433785c86558481bb8e12f350cd Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 11:24:28 -0500 Subject: [PATCH 40/62] remove --- app/javascript/packages/webauthn/webauthn-input-element.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index af71c0edd0f..652f7423cd6 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,6 +1,5 @@ import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; -import { trackEvent } from '@18f/identity-analytics'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { @@ -33,8 +32,8 @@ export class WebauthnInputElement extends HTMLElement { this.classList.add('webauthn-input--unsupported-passkey'); } - if(this.isOptedInToAbTest && this.hasAttribute('hidden')) { - trackEvent('desktop_ab_test_option_shown') + if(!this.isOptedInToAbTest) { + this.hidden = true; } } } From ae0222a521bb1ed054998afdc031c29834a4338f Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 15:46:55 -0500 Subject: [PATCH 41/62] rename bucket, remove a/b test setup from log in files, pass bucket percentage to sign up screen --- .../two_factor_authentication/options_controller.rb | 1 - .../set_up_webauthn_platform_selection_presenter.rb | 2 +- .../sign_in_webauthn_platform_selection_presenter.rb | 1 - app/presenters/two_factor_login_options_presenter.rb | 7 ++----- app/presenters/two_factor_options_presenter.rb | 3 ++- config/initializers/ab_tests.rb | 2 +- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb index 1dde5af97ea..1871073aa52 100644 --- a/app/controllers/two_factor_authentication/options_controller.rb +++ b/app/controllers/two_factor_authentication/options_controller.rb @@ -55,7 +55,6 @@ def two_factor_options_presenter 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?, - desktop_ab_test_option: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP), ) end diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index 5a26c6d1f94..1e7ff563da8 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -13,7 +13,7 @@ def render_in(view_context, &block) passkey_supported_only: true, show_unsupported_passkey: IdentityConfig.store.show_unsupported_passkey_platform_authentication_setup, - desktop_ft_unlock_option: true, + desktop_ft_unlock_option: desktop_ft_ab_test, ), &block ) diff --git a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb index 4ea1c2e473d..61024f9b02f 100644 --- a/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sign_in_webauthn_platform_selection_presenter.rb @@ -12,7 +12,6 @@ def render_in(view_context, &block) platform: true, passkey_supported_only: false, show_unsupported_passkey: false, - desktop_ft_unlock_option: true, ), &block ) diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index a59d8e874f3..bcc13ba0747 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -8,8 +8,7 @@ class TwoFactorLoginOptionsPresenter < TwoFactorAuthCode::GenericDeliveryPresent :reauthentication_context, :phishing_resistant_required, :piv_cac_required, - :add_piv_cac_after_2fa, - :desktop_ab_test_option + :add_piv_cac_after_2fa alias_method :reauthentication_context?, :reauthentication_context alias_method :phishing_resistant_required?, :phishing_resistant_required @@ -23,8 +22,7 @@ def initialize( service_provider:, phishing_resistant_required:, piv_cac_required:, - add_piv_cac_after_2fa:, - desktop_ab_test_option: nil + add_piv_cac_after_2fa: ) @user = user @view = view @@ -33,7 +31,6 @@ def initialize( @phishing_resistant_required = phishing_resistant_required @piv_cac_required = piv_cac_required @add_piv_cac_after_2fa = add_piv_cac_after_2fa - @desktop_ab_test_option = desktop_ab_test_option end def title diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 02b8d9c4225..13ffabc1e30 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -49,7 +49,8 @@ def all_options_sorted user:, piv_cac_required: piv_cac_required?, phishing_resistant_required: phishing_resistant_only?, - user_agent: + user_agent:, + desktop_ft_ab_test:, ) end. partition(&:recommended?). diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index e3813ab5b0e..d8bde27dbef 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -109,7 +109,7 @@ def self.all 'WebAuthn Setup Visited', 'Multi-Factor Authentication Setup', ].to_set, - buckets: { desktop_ft_unlock_setup: + buckets: { desktop_ft_unlock_option_shown: IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested }, ).freeze end From abb787debc836f4fe6f395d6aeff86704c74d8d7 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 8 Nov 2024 15:58:24 -0500 Subject: [PATCH 42/62] lintfix --- .../packages/webauthn/webauthn-input-element.spec.ts | 12 ++++++------ .../packages/webauthn/webauthn-input-element.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 3c804f39b27..d7841aa7083 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -90,7 +90,7 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { - const sandbox = useSandbox() + const sandbox = useSandbox(); beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); sandbox.stub(analytics, 'trackEvent'); @@ -103,8 +103,8 @@ describe('WebauthnInputElement', () => { }); it('does not log the event', () => { - expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown') - }) + expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); + }); }); context('desktop F/T unlock setup disabled', () => { @@ -113,7 +113,7 @@ describe('WebauthnInputElement', () => { document.body.innerHTML = ``; }); - it('is hidden when passkeys are supported',() => { + it('is hidden when passkeys are supported', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); @@ -121,7 +121,7 @@ describe('WebauthnInputElement', () => { }); context('when the setup option would be hidden', () => { - const sandbox = useSandbox() + const sandbox = useSandbox(); beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; @@ -131,6 +131,6 @@ describe('WebauthnInputElement', () => { it('logs the event', () => { expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); }); - }) + }); }); }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 652f7423cd6..37a9a9d6f82 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -32,7 +32,7 @@ export class WebauthnInputElement extends HTMLElement { this.classList.add('webauthn-input--unsupported-passkey'); } - if(!this.isOptedInToAbTest) { + if (!this.isOptedInToAbTest) { this.hidden = true; } } From d38d7ba4ce6cb32f65074175fd2f067a11a85ca7 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 08:35:06 -0500 Subject: [PATCH 43/62] fix js test --- .../webauthn/webauthn-input-element.spec.ts | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index d7841aa7083..7910e0589e1 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -90,7 +90,7 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { - const sandbox = useSandbox(); + const sandbox = useSandbox() beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); sandbox.stub(analytics, 'trackEvent'); @@ -101,36 +101,20 @@ describe('WebauthnInputElement', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); }); - - it('does not log the event', () => { - expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); - }); }); context('desktop F/T unlock setup disabled', () => { - beforeEach(() => { - isWebauthnPlatformAvailable.resolves(false); - document.body.innerHTML = ``; - }); - - it('is hidden when passkeys are supported', () => { - const element = document.querySelector('lg-webauthn-input')!; - - expect(element.hidden).to.be.false(); - }); - }); - - context('when the setup option would be hidden', () => { - const sandbox = useSandbox(); + const sandbox = useSandbox() beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); - document.body.innerHTML = ``; + document.body.innerHTML = ``; sandbox.stub(analytics, 'trackEvent'); }); - it('logs the event', () => { - expect(analytics.trackEvent).to.not.have.been.calledWith('desktop_ab_test_option_shown'); - }); - }); + it('is hidden', () => { + const element = document.querySelector('lg-webauthn-input')!; + expect(element.hidden).to.be.true(); + }) + }) }); }); From 431645c315e6c99c8372d8ce86dc1dba9550499a Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 09:06:09 -0500 Subject: [PATCH 44/62] js lintfixes --- .../packages/webauthn/webauthn-input-element.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 7910e0589e1..8bbd36edd28 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -90,7 +90,7 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { - const sandbox = useSandbox() + const sandbox = useSandbox(); beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); sandbox.stub(analytics, 'trackEvent'); @@ -104,7 +104,7 @@ describe('WebauthnInputElement', () => { }); context('desktop F/T unlock setup disabled', () => { - const sandbox = useSandbox() + const sandbox = useSandbox(); beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; @@ -114,7 +114,7 @@ describe('WebauthnInputElement', () => { it('is hidden', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.true(); - }) - }) + }); + }); }); }); From 08396d915daa6b91d8e2eba9ef3c1628e6acb595 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 10:08:09 -0500 Subject: [PATCH 45/62] fix F/T unlock show logic --- app/components/webauthn_input_component.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 1eaec1e5cc3..ac8193ec004 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -35,7 +35,7 @@ def call end def initial_hidden_tag_options - if platform? && passkey_supported_only? || !desktop_ft_unlock_option? + if platform? && passkey_supported_only? { hidden: true } else { class: 'js' } @@ -46,7 +46,7 @@ def show_desktop_ft_unlock_option? if desktop_ft_unlock_option? && I18n.locale == :en true else - false + { hidden: true } end end end From 6c9a5025be073f0a81cf533758ebcea3e40521c3 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 10:27:02 -0500 Subject: [PATCH 46/62] fix for f/t unlock logic? --- app/components/webauthn_input_component.rb | 2 +- config/application.yml.default | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index ac8193ec004..d510f420234 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -46,7 +46,7 @@ def show_desktop_ft_unlock_option? if desktop_ft_unlock_option? && I18n.locale == :en true else - { hidden: true } + false end end end diff --git a/config/application.yml.default b/config/application.yml.default index e87e80e6cb7..f51466015dd 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -458,7 +458,7 @@ development: compromised_password_randomizer_value: 1 dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers - desktop_ft_unlock_setup_option_percent_tested: 100 + desktop_ft_unlock_setup_option_percent_tested: 0 doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false From 5371ae7d30b8da3f4214637db824bf716aee4eef Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 11:41:31 -0500 Subject: [PATCH 47/62] fix javascript test --- .../webauthn/webauthn-input-element.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 8bbd36edd28..f5fd2c5b270 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -42,6 +42,20 @@ describe('WebauthnInputElement', () => { }); }); + context('part of A/B test', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.resolves(true); + document.body.innerHTML = ``; + }); + + it('becomes visible', async () => { + const element = document.querySelector('lg-webauthn-input')!; + + await waitFor(() => expect(element.hidden).to.be.false()); + }); + }); + context('unsupported passkey shown', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); From d8ffe62a3c64a65a8f83ac10bd80272388ae0a79 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 12:37:58 -0500 Subject: [PATCH 48/62] remove unneeded logic from input element, remove `trackEvent` --- .../packages/webauthn/webauthn-input-element.spec.ts | 3 --- app/javascript/packages/webauthn/webauthn-input-element.ts | 4 ---- 2 files changed, 7 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index f5fd2c5b270..955af903189 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -2,7 +2,6 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; import { useSandbox } from '@18f/identity-test-helpers'; -import * as analytics from '@18f/identity-analytics'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; @@ -107,7 +106,6 @@ describe('WebauthnInputElement', () => { const sandbox = useSandbox(); beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); - sandbox.stub(analytics, 'trackEvent'); document.body.innerHTML = ``; }); @@ -122,7 +120,6 @@ describe('WebauthnInputElement', () => { beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; - sandbox.stub(analytics, 'trackEvent'); }); it('is hidden', () => { diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 37a9a9d6f82..37c8210693f 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -31,10 +31,6 @@ export class WebauthnInputElement extends HTMLElement { this.hidden = false; this.classList.add('webauthn-input--unsupported-passkey'); } - - if (!this.isOptedInToAbTest) { - this.hidden = true; - } } } From 91612ca0b5c156a55269a209f0ecd1447ca89007 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 14:55:04 -0500 Subject: [PATCH 49/62] lintfix, change default config number --- .../packages/webauthn/webauthn-input-element.spec.ts | 5 ++--- config/application.yml.default | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 955af903189..ca7f7192dc8 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,7 +1,6 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; -import { useSandbox } from '@18f/identity-test-helpers'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; @@ -103,7 +102,7 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { - const sandbox = useSandbox(); + beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; @@ -116,7 +115,7 @@ describe('WebauthnInputElement', () => { }); context('desktop F/T unlock setup disabled', () => { - const sandbox = useSandbox(); + beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; diff --git a/config/application.yml.default b/config/application.yml.default index f51466015dd..e87e80e6cb7 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -458,7 +458,7 @@ development: compromised_password_randomizer_value: 1 dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers - desktop_ft_unlock_setup_option_percent_tested: 0 + desktop_ft_unlock_setup_option_percent_tested: 100 doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false From 45b7d5a5eb77aef75e8d89aa4c592e92f4ed06e6 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Tue, 12 Nov 2024 15:15:29 -0500 Subject: [PATCH 50/62] more lintfixes --- app/javascript/packages/webauthn/webauthn-input-element.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index ca7f7192dc8..526e0d35429 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -102,7 +102,6 @@ describe('WebauthnInputElement', () => { context('Desktop F/T unlock A/B test', () => { context('desktop F/T unlock setup enabled', () => { - beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; @@ -115,7 +114,6 @@ describe('WebauthnInputElement', () => { }); context('desktop F/T unlock setup disabled', () => { - beforeEach(() => { isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; From 8f7e0590a1af8d67c293f6ff6ff56e0b4b59888e Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 13 Nov 2024 13:46:09 -0500 Subject: [PATCH 51/62] clean up hidden and webauthn-input-element specs --- .../webauthn/webauthn-input-element.spec.ts | 2 +- spec/features/webauthn/hidden_spec.rb | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 526e0d35429..7b8c893fa99 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -40,7 +40,7 @@ describe('WebauthnInputElement', () => { }); }); - context('part of A/B test', () => { + context('as a part of A/B test', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); isWebauthnPlatformAvailable.resolves(true); diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index bb5f9240ad4..0c0ea800316 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -67,15 +67,26 @@ reload_ab_tests end - after do + it 'displays the authenticator option' do + sign_up_and_set_password + simulate_platform_authenticator_available + + expect(webauthn_option_hidden?).to eq(false) + end + end + + context 'when A/B test is disabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(0) reload_ab_tests end - it 'displays the authenticator option' do + it 'hides the authenticator option' do sign_up_and_set_password simulate_platform_authenticator_available - expect(webauthn_option_hidden?).to eq(false) + expect(webauthn_option_hidden?).to eq(true) end end From 4be840ab1738c6b3e1b543c98fe09c6a2f32f308 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 13 Nov 2024 16:41:35 -0500 Subject: [PATCH 52/62] Update app/components/webauthn_input_component.rb Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- app/components/webauthn_input_component.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index d510f420234..f029d9f1c8f 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -43,10 +43,6 @@ def initial_hidden_tag_options end def show_desktop_ft_unlock_option? - if desktop_ft_unlock_option? && I18n.locale == :en - true - else - false - end + desktop_ft_unlock_option? && I18n.locale == :en end end From f63587ec18f86bcbed53591aa1b353f0d9a84bb4 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 14 Nov 2024 09:59:31 -0500 Subject: [PATCH 53/62] Update app/javascript/packages/webauthn/webauthn-input-element.ts Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- app/javascript/packages/webauthn/webauthn-input-element.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 37c8210693f..4bde518f761 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -19,12 +19,11 @@ export class WebauthnInputElement extends HTMLElement { } async toggleVisibleIfPasskeySupported() { - const webauthnPlatformAvailable = await isWebauthnPlatformAuthenticatorAvailable(); - if (!this.hasAttribute('hidden')) { return; } + const webauthnPlatformAvailable = await isWebauthnPlatformAuthenticatorAvailable(); if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && webauthnPlatformAvailable) { this.hidden = false; } else if (this.showUnsupportedPasskey) { From e2332aa8a76667b64b1181f14b412b28db057d9a Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 14 Nov 2024 10:44:54 -0500 Subject: [PATCH 54/62] update test --- app/javascript/packages/webauthn/webauthn-input-element.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 7b8c893fa99..76ca39a753e 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -65,7 +65,7 @@ describe('WebauthnInputElement', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.true(); - expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.false(); + expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); }); }); }); From f2328669af0547da187f7c8a7fc2121a06a555c0 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Thu, 14 Nov 2024 14:54:54 -0500 Subject: [PATCH 55/62] delete unused analytics event --- app/services/analytics_events.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 9c032842fcf..240bb5b40ad 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -451,11 +451,6 @@ def create_new_device_alert_job_emails_sent(count:, **extra) track_event(:create_new_device_alert_job_emails_sent, count:, **extra) end - # For those in the desktop F/T unlock bucket, tracks when the user sees the F/T unlock option - def desktop_ab_test_option_shown - track_event(:desktop_ab_test_option_shown) - end - # @param [String] message the warning # @param [Array] unknown_alerts Names of alerts not recognized by our code # @param [Hash] response_info Response payload From dea11a4f6900a88ae0755de29f3b529e34cb2cc2 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 15 Nov 2024 15:47:35 -0500 Subject: [PATCH 56/62] change analytics event, remove duplicate a/b test --- .../webauthn/webauthn-input-element.spec.ts | 26 ------------------- config/initializers/ab_tests.rb | 2 +- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 76ca39a753e..86119f0ec96 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -99,30 +99,4 @@ describe('WebauthnInputElement', () => { }); }); }); - - context('Desktop F/T unlock A/B test', () => { - context('desktop F/T unlock setup enabled', () => { - beforeEach(() => { - isWebauthnPlatformAvailable.resolves(true); - document.body.innerHTML = ``; - }); - - it('becomes visible', () => { - const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); - }); - }); - - context('desktop F/T unlock setup disabled', () => { - beforeEach(() => { - isWebauthnPlatformAvailable.resolves(true); - document.body.innerHTML = ``; - }); - - it('is hidden', () => { - const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.true(); - }); - }); - }); }); diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 33553204a50..8ed3c40e992 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -106,7 +106,7 @@ def self.all DESKTOP_FT_UNLOCK_SETUP = AbTest.new( experiment_name: 'Desktop F/T unlock setup', should_log: [ - 'WebAuthn Setup Visited', + :webauthn_setup_submitted, 'Multi-Factor Authentication Setup', ].to_set, buckets: { desktop_ft_unlock_option_shown: From 50296f6a4f8e2e0f188d736e324cad18fb2c1463 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 15 Nov 2024 16:24:42 -0500 Subject: [PATCH 57/62] do check for bucket type --- .../users/two_factor_authentication_setup_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 015d3f53a5d..e2053ec8118 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -68,7 +68,8 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, - desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP), + desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == + (:desktop_ft_unlock_option_shown), ) end From ec5f9a41411b8a1aa61a331b3be5508a1a18dec4 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Fri, 15 Nov 2024 16:40:30 -0500 Subject: [PATCH 58/62] set up method if in bucket and test is running --- .../two_factor_authentication_setup_controller.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index e2053ec8118..0b0e6d316ee 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -68,8 +68,7 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, - desktop_ft_ab_test: ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == - (:desktop_ft_unlock_option_shown), + desktop_ft_ab_test: isInAbTestBucket?, ) end @@ -83,5 +82,12 @@ def two_factor_options_form_params rescue ActionController::ParameterMissing ActionController::Parameters.new(selection: []) end + + def isInAbTestBucket? + testing_bucket = ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == :desktop_ft_unlock_option_shown + test_enabled = IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested > 0 + + testing_bucket && test_enabled + end end end From 2102310869c3c96c0ff8f9fb24d9c87bd7d0b92c Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 18 Nov 2024 11:49:32 -0500 Subject: [PATCH 59/62] check for A/B test flag --- .../two_factor_authentication_setup_controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 0b0e6d316ee..9e6e9667b1d 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -5,6 +5,7 @@ class TwoFactorAuthenticationSetupController < ApplicationController include UserAuthenticator include MfaSetupConcern include AbTestingConcern + include ApplicationHelper before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup @@ -68,7 +69,7 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, - desktop_ft_ab_test: isInAbTestBucket?, + desktop_ft_ab_test: isInAbTestBucket, ) end @@ -83,11 +84,9 @@ def two_factor_options_form_params ActionController::Parameters.new(selection: []) end - def isInAbTestBucket? - testing_bucket = ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == :desktop_ft_unlock_option_shown - test_enabled = IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested > 0 - - testing_bucket && test_enabled + def isInAbTestBucket + ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == (:desktop_ft_unlock_option_shown) && + desktop_device? end end end From 97575ed4f4109dd0f2817b42b861d45905e9420b Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 18 Nov 2024 12:36:08 -0500 Subject: [PATCH 60/62] For bucket check, change value type and fix test --- app/presenters/two_factor_options_presenter.rb | 2 +- .../two_factor_authentication_setup_controller_spec.rb | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 13ffabc1e30..1dc080b59e4 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -20,7 +20,7 @@ def initialize( show_skip_additional_mfa_link: true, after_mfa_setup_path: nil, return_to_sp_cancel_path: nil, - desktop_ft_ab_test: nil + desktop_ft_ab_test: false ) @user_agent = user_agent @user = user diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index d5adeb5fe2a..574627a8860 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -21,10 +21,10 @@ ) end - it 'initializes presenter with blank ab test bucket value' do + it 'initializes presenter with false ab test bucket value' do response - expect(assigns(:presenter).desktop_ft_ab_test).to be_nil + expect(assigns(:presenter).desktop_ft_ab_test).to be false end context 'with user having gov or mil email' do @@ -112,13 +112,15 @@ context 'with user opted in to desktop ft unlock setup ab test' do before do - allow(controller).to receive(:ab_test_bucket).and_return(:desktop_ft_unlock_setup) + allow(controller).to receive(:ab_test_bucket).with( + :DESKTOP_FT_UNLOCK_SETUP, + ).and_return(:desktop_ft_unlock_option_shown) end it 'initializes presenter with ab test bucket value' do response - expect(assigns(:presenter).desktop_ft_ab_test).to eq(:desktop_ft_unlock_setup) + expect(assigns(:presenter).desktop_ft_ab_test).to eq(true) end end end From c992466c4b00bdfbeda7469261e50e4ab88629cc Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 18 Nov 2024 15:33:25 -0500 Subject: [PATCH 61/62] fix js test --- .../packages/webauthn/webauthn-input-element.spec.ts | 2 +- app/javascript/packages/webauthn/webauthn-input-element.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 86119f0ec96..641058c2ec3 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -64,7 +64,7 @@ describe('WebauthnInputElement', () => { it('becomes visible, with modifier class', () => { const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.true(); + expect(element.hidden).to.be.false(); expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); }); }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 4bde518f761..9d995108046 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -23,8 +23,7 @@ export class WebauthnInputElement extends HTMLElement { return; } - const webauthnPlatformAvailable = await isWebauthnPlatformAuthenticatorAvailable(); - if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && webauthnPlatformAvailable) { + if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From 79201362927016ada220323c7703c1b82974ae24 Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Mon, 18 Nov 2024 15:56:43 -0500 Subject: [PATCH 62/62] lintfix --- app/javascript/packages/webauthn/webauthn-input-element.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 9d995108046..a7e9759b6c4 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -23,7 +23,10 @@ export class WebauthnInputElement extends HTMLElement { return; } - if ((isWebauthnPasskeySupported() || this.isOptedInToAbTest) && (await isWebauthnPlatformAuthenticatorAvailable())) { + if ( + (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && + (await isWebauthnPlatformAuthenticatorAvailable()) + ) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false;