Skip to content

Commit

Permalink
LG-14655: A/B test to recommend platform authenticator to SMS users (#…
Browse files Browse the repository at this point in the history
…11402)

* LG-14655: A/B test to recommend platform authenticator to SMS users

changelog: Internal, Upcoming Features, Create A/B test to recommend platform authenticator to SMS users

* Restore test case

Mistakenly removed in rebase

* Convert "should" to "expect" syntax

* Update experiment name

Standardize on "recommend" terminology, also broaden to cover account creation flow

* Sort analytics methods

* Check in multi-MFA setup flow for suggesting second MFA

* Remove redundant, inaccurate test case

* Add specs for RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER AbTest

* Split configuration for authentication vs. account creation
  • Loading branch information
aduth authored Nov 4, 2024
1 parent 15b8fee commit 18917fe
Show file tree
Hide file tree
Showing 30 changed files with 709 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,11 @@ Rails/SelectMap:
Rails/ShortI18n:
Enabled: true

Rails/SkipsModelValidations:
Enabled: true
Exclude:
- 'spec/**/*.rb'

Rails/StripHeredoc:
Enabled: false

Expand Down
3 changes: 3 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ def after_sign_in_path_for(_user)
return login_add_piv_cac_prompt_url if session[:needs_to_setup_piv_cac_after_sign_in].present?
return reactivate_account_url if user_needs_to_reactivate_account?
return login_piv_cac_recommended_path if user_recommended_for_piv_cac?
return webauthn_platform_recommended_path if recommend_webauthn_platform_for_sms_user?(
:recommend_for_authentication,
)
return second_mfa_reminder_url if user_needs_second_mfa_reminder?
return sp_session_request_url_with_updated_params if sp_session.key?(:request_url)
signed_in_url
Expand Down
14 changes: 9 additions & 5 deletions app/controllers/concerns/mfa_setup_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

module MfaSetupConcern
extend ActiveSupport::Concern
include RecommendWebauthnPlatformConcern

def next_setup_path
if suggest_second_mfa?
auth_method_confirmation_url
elsif next_setup_choice
if next_setup_choice
confirmation_path
elsif recommend_webauthn_platform_for_sms_user?(:recommend_for_account_creation)
webauthn_platform_recommended_path
elsif suggest_second_mfa?
auth_method_confirmation_path
elsif user_session[:mfa_selections]
track_user_registration_mfa_setup_complete_event
user_session.delete(:mfa_selections)
Expand Down Expand Up @@ -52,8 +55,9 @@ def mfa_context
end

def suggest_second_mfa?
return false unless user_session[:mfa_selections]
mfa_selection_count < 2 && mfa_context.enabled_mfa_methods_count < 2
return false if !in_multi_mfa_selection_flow?
return false if current_user.webauthn_platform_recommended_dismissed_at?
mfa_context.enabled_mfa_methods_count < 2
end

def first_mfa_selection_path
Expand Down
39 changes: 39 additions & 0 deletions app/controllers/concerns/recommend_webauthn_platform_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module RecommendWebauthnPlatformConcern
def recommend_webauthn_platform_for_sms_user?(bucket)
# Only consider for A/B test if:
# 1. Option would be offered for setup
# 2. User is viewing content in English
# 3. Other recommendations have not already been offered (e.g. PIV/CAC for federal emails)
# 4. User selected to setup phone or authenticated with phone
# 5. User has not already set up a platform authenticator
return false if !device_supports_platform_authenticator_setup?
return false if I18n.locale != :en
return false if current_user.webauthn_platform_recommended_dismissed_at?
return false if !user_set_up_or_authenticated_with_phone?
return false if current_user.webauthn_configurations.platform_authenticators.present?
ab_test_bucket(:RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER) == bucket
end

private

def device_supports_platform_authenticator_setup?
user_session[:platform_authenticator_available] == true
end

def in_account_creation_flow?
user_session[:in_account_creation_flow] == true
end

def user_set_up_or_authenticated_with_phone?
if in_account_creation_flow?
current_user.phone_configurations.any? do |phone_configuration|
phone_configuration.mfa_enabled? && phone_configuration.delivery_preference == 'sms'
end
else
auth_methods_session.auth_events.pluck(:auth_method).
include?(TwoFactorAuthenticatable::AuthMethod::SMS)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Users
class TwoFactorAuthenticationSetupController < ApplicationController
include UserAuthenticator
include MfaSetupConcern
include AbTestingConcern

before_action :authenticate_user
before_action :confirm_user_authenticated_for_2fa_setup
Expand All @@ -23,6 +24,8 @@ def index
def create
result = submit_form
analytics.user_registration_2fa_setup(**result.to_h)
user_session[:platform_authenticator_available] =
params[:platform_authenticator_available] == 'true'

if result.success?
process_valid_form
Expand Down
38 changes: 38 additions & 0 deletions app/controllers/users/webauthn_platform_recommended_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Users
class WebauthnPlatformRecommendedController < ApplicationController
include SecureHeadersConcern
include MfaSetupConcern

before_action :confirm_two_factor_authenticated
before_action :apply_secure_headers_override

def new
@sign_in_flow = session[:sign_in_flow]
analytics.webauthn_platform_recommended_visited
end

def create
analytics.webauthn_platform_recommended_submitted(opted_to_add: opted_to_add?)
current_user.update(webauthn_platform_recommended_dismissed_at: Time.zone.now)
redirect_to dismiss_redirect_path
end

private

def opted_to_add?
params[:add_method].present?
end

def dismiss_redirect_path
if opted_to_add?
webauthn_setup_path(platform: true)
elsif in_account_creation_flow?
next_setup_path
else
after_sign_in_path_for(current_user)
end
end
end
end
17 changes: 6 additions & 11 deletions app/javascript/packs/platform-authenticator-available.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ import {
isWebauthnPasskeySupported,
} from '@18f/identity-webauthn';

async function platformAuthenticatorAvailable() {
const platformAuthenticatorAvailableInput = document.getElementById(
'platform_authenticator_available',
) as HTMLInputElement;
if (!platformAuthenticatorAvailableInput) {
return;
}
export async function initialize() {
const input = document.getElementById('platform_authenticator_available') as HTMLInputElement;
if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) {
platformAuthenticatorAvailableInput.value = 'true';
} else {
platformAuthenticatorAvailableInput.value = 'false';
input.value = 'true';
}
}

platformAuthenticatorAvailable();
if (process.env.NODE_ENV !== 'test') {
initialize();
}
11 changes: 11 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7062,6 +7062,17 @@ def webauthn_delete_submitted(
)
end

# User submits WebAuthn platform authenticator recommended screen
# @param [Boolean] opted_to_add Whether the user chose to add a method
def webauthn_platform_recommended_submitted(opted_to_add:, **extra)
track_event(:webauthn_platform_recommended_submitted, opted_to_add:, **extra)
end

# User visits WebAuthn platform authenticator recommended screen
def webauthn_platform_recommended_visited
track_event(:webauthn_platform_recommended_visited)
end

# @param [Hash] platform_authenticator
# @param [Boolean] success
# @param [Hash, nil] errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
</fieldset>
</div>

<%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %>
<% javascript_packs_tag_once('platform-authenticator-available') %>

<%= f.submit t('forms.buttons.continue'), class: 'margin-bottom-1' %>
<% end %>

Expand Down
41 changes: 41 additions & 0 deletions app/views/users/webauthn_platform_recommended/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<% self.title = t('webauthn_platform_recommended.heading') %>

<%= render StatusPageComponent.new(status: :info, icon: :question) do |c| %>
<% c.with_header { t('webauthn_platform_recommended.heading') } %>

<p><%= t('webauthn_platform_recommended.description_secure_account') %></p>

<p>
<%= t(
'webauthn_platform_recommended.description_private_html',
phishing_resistant_link_html: new_tab_link_to(
t('webauthn_platform_recommended.phishing_resistant'),
help_center_redirect_path(
category: 'get-started',
article: 'authentication-methods',
anchor: 'face-or-touch-unlock',
flow: @sign_in_flow,
step: :webauthn_platform_recommended,
),
),
) %>
</p>

<div class="grid-row margin-top-5">
<div class="tablet:grid-col-9">
<%= render ButtonComponent.new(
url: webauthn_platform_recommended_url,
method: :post,
params: { add_method: true },
big: true,
full_width: true,
class: 'margin-bottom-2',
).with_content(t('webauthn_platform_recommended.cta')) %>
<%= render ButtonComponent.new(
url: webauthn_platform_recommended_url,
method: :post,
unstyled: true,
).with_content(t('webauthn_platform_recommended.skip')) %>
</div>
</div>
<% end %>
2 changes: 2 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ recaptcha_enterprise_project_id: ''
recaptcha_mock_validator: true
recaptcha_secret_key: ''
recaptcha_site_key: ''
recommend_webauthn_platform_for_sms_ab_test_account_creation_percent: 0
recommend_webauthn_platform_for_sms_ab_test_authentication_percent: 0
recovery_code_length: 4
redis_pool_size: 10
redis_throttle_pool_size: 5
Expand Down
15 changes: 15 additions & 0 deletions config/initializers/ab_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,19 @@ def self.all
should_log: ['Password Reset: Password Submitted'].to_set,
buckets: { log: IdentityConfig.store.log_password_reset_matches_existing_ab_test_percent },
).freeze

RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER = AbTest.new(
experiment_name: 'Recommend Face or Touch Unlock for SMS users',
should_log: [
'Multi-Factor Authentication',
'User Registration: MFA Setup Complete',
'User Registration: 2FA Setup',
].to_set,
buckets: {
recommend_for_account_creation:
IdentityConfig.store.recommend_webauthn_platform_for_sms_ab_test_account_creation_percent,
recommend_for_authentication:
IdentityConfig.store.recommend_webauthn_platform_for_sms_ab_test_authentication_percent,
},
).freeze
end
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,12 @@ vendor_outage.blocked.phone.default: We cannot verify phones at this time. Pleas
vendor_outage.get_updates: Get updates
vendor_outage.get_updates_on_status_page: Get updates on our status page
vendor_outage.working: We are working to resolve an error
webauthn_platform_recommended.cta: Set up face or touch unlock
webauthn_platform_recommended.description_private_html: Face or touch unlock is %{phishing_resistant_link_html} and we don’t store any recordings of your face or fingerprint, so your information stays private.
webauthn_platform_recommended.description_secure_account: Secure your sign in with face or touch unlock. Use your face, fingerprint, password, or another method to keep your account safer.
webauthn_platform_recommended.heading: Set up face or touch unlock for a more secure sign in
webauthn_platform_recommended.phishing_resistant: phishing-resistant
webauthn_platform_recommended.skip: Skip
zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: A word by itself is easy to guess
zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Add another word or two. Uncommon words are better
zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: All-uppercase is almost as easy to guess as all-lowercase
Expand Down
6 changes: 6 additions & 0 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2019,6 +2019,12 @@ vendor_outage.blocked.phone.default: No podemos verificar teléfonos en estos mo
vendor_outage.get_updates: Obtenga actualizaciones
vendor_outage.get_updates_on_status_page: Obtenga las actualizaciones en nuestra página de estado
vendor_outage.working: Estamos trabajando para corregir un error
webauthn_platform_recommended.cta: Set up face or touch unlock
webauthn_platform_recommended.description_private_html: Face or touch unlock is %{phishing_resistant_link_html} and we don’t store any recordings of your face or fingerprint, so your information stays private.
webauthn_platform_recommended.description_secure_account: Secure your sign in with face or touch unlock. Use your face, fingerprint, password, or another method to keep your account safer.
webauthn_platform_recommended.heading: Set up face or touch unlock for a more secure sign in
webauthn_platform_recommended.phishing_resistant: phishing-resistant
webauthn_platform_recommended.skip: Skip
zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: Una sola palabra es fácil de adivinar.
zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Añada otra palabra o dos. Es mejor usar palabras poco comunes.
zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Todo en mayúsculas es casi tan fácil de adivinar como todo en minúsculas.
Expand Down
6 changes: 6 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,12 @@ vendor_outage.blocked.phone.default: Nous ne sommes pas actuellement en mesure d
vendor_outage.get_updates: Obtenir les dernières informations
vendor_outage.get_updates_on_status_page: Obtenir les dernières informations sur notre page d’état des systèmes.
vendor_outage.working: Nous travaillons à la résolution d’une erreur
webauthn_platform_recommended.cta: Set up face or touch unlock
webauthn_platform_recommended.description_private_html: Face or touch unlock is %{phishing_resistant_link_html} and we don’t store any recordings of your face or fingerprint, so your information stays private.
webauthn_platform_recommended.description_secure_account: Secure your sign in with face or touch unlock. Use your face, fingerprint, password, or another method to keep your account safer.
webauthn_platform_recommended.heading: Set up face or touch unlock for a more secure sign in
webauthn_platform_recommended.phishing_resistant: phishing-resistant
webauthn_platform_recommended.skip: Skip
zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: Un mot seul est facile à deviner
zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: Ajoutez un ou deux autres mots. Il est préférable d’utiliser des mots peu communs.
zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Tout en majuscules est presque aussi facile à deviner que tout en minuscules.
Expand Down
6 changes: 6 additions & 0 deletions config/locales/zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2020,6 +2020,12 @@ vendor_outage.blocked.phone.default: 我们目前无法验证电话。请稍后
vendor_outage.get_updates: 获得最新信息
vendor_outage.get_updates_on_status_page: 在我们的状态页面获得最新信息。
vendor_outage.working: 我们正在争取解决错误。
webauthn_platform_recommended.cta: Set up face or touch unlock
webauthn_platform_recommended.description_private_html: Face or touch unlock is %{phishing_resistant_link_html} and we don’t store any recordings of your face or fingerprint, so your information stays private.
webauthn_platform_recommended.description_secure_account: Secure your sign in with face or touch unlock. Use your face, fingerprint, password, or another method to keep your account safer.
webauthn_platform_recommended.heading: Set up face or touch unlock for a more secure sign in
webauthn_platform_recommended.phishing_resistant: phishing-resistant
webauthn_platform_recommended.skip: Skip
zxcvbn.feedback.a_word_by_itself_is_easy_to_guess: 单字容易被人猜出
zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better: 再加一两个字不常见的字更好
zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: 都是大写几乎和都是小写一样容易被人猜出
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@
get '/second_mfa_reminder' => 'users/second_mfa_reminder#new'
post '/second_mfa_reminder' => 'users/second_mfa_reminder#create'

get '/webauthn_platform_recommended' => 'users/webauthn_platform_recommended#new'
post '/webauthn_platform_recommended' => 'users/webauthn_platform_recommended#create'

get '/piv_cac' => 'users/piv_cac_authentication_setup#new', as: :setup_piv_cac
get '/piv_cac_error' => 'users/piv_cac_authentication_setup#error', as: :setup_piv_cac_error
post '/present_piv_cac' => 'users/piv_cac_authentication_setup#submit_new_piv_cac',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddWebauthnPlatformRecommendedDismissedAtToUser < ActiveRecord::Migration[7.2]
def change
add_column :users, :webauthn_platform_recommended_dismissed_at, :datetime, default: nil, comment: 'sensitive=false'
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@
t.datetime "piv_cac_recommended_dismissed_at", comment: "sensitive=false"
t.datetime "sign_in_new_device_at", comment: "sensitive=false"
t.datetime "password_compromised_checked_at", comment: "sensitive=false"
t.datetime "webauthn_platform_recommended_dismissed_at", comment: "sensitive=false"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["sign_in_new_device_at"], name: "index_users_on_sign_in_new_device_at"
t.index ["uuid"], name: "index_users_on_uuid", unique: true
Expand Down
4 changes: 4 additions & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def self.store

# identity-hostdata transforms these configs to the described type
# rubocop:disable Metrics/BlockLength
# rubocop:disable Metrics/LineLength
BUILDER = proc do |config|
# ______________________________________
# / Adding something new in here? Please \
Expand Down Expand Up @@ -401,6 +402,8 @@ def self.store
config.add(:sign_in_recaptcha_percent_tested, type: :integer)
config.add(:sign_in_recaptcha_score_threshold, type: :float)
config.add(:skip_encryption_allowed_list, type: :json)
config.add(:recommend_webauthn_platform_for_sms_ab_test_account_creation_percent, type: :integer)
config.add(:recommend_webauthn_platform_for_sms_ab_test_authentication_percent, type: :integer)
config.add(:socure_document_request_endpoint, type: :string)
config.add(:socure_enabled, type: :boolean)
config.add(:socure_idplus_api_key, type: :string)
Expand Down Expand Up @@ -467,5 +470,6 @@ def self.store
config.add(:vtm_url)
config.add(:weekly_auth_funnel_report_config, type: :json)
end.freeze
# rubocop:enable Metrics/LineLength
# rubocop:enable Metrics/BlockLength
end
Loading

0 comments on commit 18917fe

Please sign in to comment.