Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LG-14396 threat metrix create account device profiling #11278

Merged
merged 21 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 0 additions & 51 deletions app/controllers/concerns/idv/threat_metrix_concern.rb

This file was deleted.

49 changes: 49 additions & 0 deletions app/controllers/concerns/threat_metrix_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module ThreatMetrixConcern
THREAT_METRIX_DOMAIN = 'h.online-metrix.net'
THREAT_METRIX_WILDCARD_DOMAIN = '*.online-metrix.net'

def override_csp_for_threat_metrix
return unless FeatureManagement.proofing_device_profiling_collecting_enabled?

threat_metrix_csp_overrides
end

def threat_metrix_csp_overrides
policy = current_content_security_policy

# ThreatMetrix requires additional Content Security Policy (CSP)
# directives to be added to the response to enable its JS to run
# in the browser.

# `script-src` must be updated to enable:
# - The domain hosting ThreatMetrix JS (so it can be included on the page)
# - `unsafe-eval`, since the ThreatMetrix JS uses eval() internally.
policy.script_src(*policy.script_src.to_set.merge([THREAT_METRIX_DOMAIN, :unsafe_eval]))

# `style-src` must be updated to enable:
# - `unsafe-inline`, since the ThreatMetrix library applies inline
# styles to elements it inserts into the DOM
request.content_security_policy_nonce_directives =
request.content_security_policy_nonce_directives.without('style-src')
policy.style_src(*(policy.style_src.to_set << :unsafe_inline))

# `img-src` must be updated to enable:
# - A wildcard domain, since the JS loads images from different
# subdomains of the main ThreatMetrix domain.
policy.img_src(*(policy.img_src.to_set << THREAT_METRIX_WILDCARD_DOMAIN))

# `connect-src` must be updated to enable:
# - The domain hosting ThreatMetrix JS, since ThreatMetrix makes XHR
# requests to this domain.
policy.connect_src(*(policy.connect_src.to_set << THREAT_METRIX_DOMAIN))

# `child-src` must be updated to enable:
# - The domain hosting ThreatMetrix JS, which used to load a fallback
# `<iframe>` element when Javascript is disabled.
policy.child_src(*(policy.child_src.to_set << THREAT_METRIX_DOMAIN))

request.content_security_policy = policy
end
end
2 changes: 1 addition & 1 deletion app/controllers/idv/in_person_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class InPersonController < ApplicationController

include IdvSessionConcern
include Flow::FlowStateMachine
include Idv::ThreatMetrixConcern
include ThreatMetrixConcern

before_action :redirect_if_flow_completed

Expand Down
20 changes: 19 additions & 1 deletion app/controllers/sign_up/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
module SignUp
class RegistrationsController < ApplicationController
include ApplicationHelper # for ial2_requested?
include ThreatMetrixHelper
include ThreatMetrixConcern

before_action :confirm_two_factor_authenticated, only: [:destroy_confirm]
before_action :require_no_authentication
before_action :redirect_if_ial2_and_idv_unavailable
before_action :override_csp_for_threat_metrix

CREATE_ACCOUNT = 'create_account'

def new
@register_user_email_form = RegisterUserEmailForm.new(analytics:)
analytics.user_registration_enter_email_visit
render :new, formats: :html
render :new, formats: :html, locals: threatmetrix_variables
end

def create
Expand Down Expand Up @@ -64,5 +67,20 @@ def redirect_if_ial2_and_idv_unavailable
redirect_to idv_unavailable_path(from: CREATE_ACCOUNT)
end
end

def threatmetrix_variables
return {} unless FeatureManagement.account_creation_device_profiling_collecting_enabled?
session_id = generate_threatmetrix_session_id

{
threatmetrix_session_id: session_id,
threatmetrix_javascript_urls: threatmetrix_javascript_urls(session_id),
threatmetrix_iframe_url: threatmetrix_iframe_url(session_id),
}
end

def generate_threatmetrix_session_id
session[:threatmetrix_session_id] ||= SecureRandom.uuid
end
end
end
32 changes: 1 addition & 31 deletions app/services/idv/steps/threat_metrix_step_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Idv
module Steps
module ThreatMetrixStepHelper
include ThreatMetrixHelper
def threatmetrix_view_variables(updating_ssn)
session_id = generate_threatmetrix_session_id(updating_ssn)

Expand All @@ -28,37 +29,6 @@ def should_generate_new_threatmetrix_session_id?(updating_ssn)
true
end
end

# @return [Array<String>]
def threatmetrix_javascript_urls(session_id)
sources = if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled
Rails.application.config.asset_sources.get_sources('mock-device-profiling')
else
['https://h.online-metrix.net/fp/tags.js']
end

sources.map do |source|
UriService.add_params(
source,
org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id,
session_id: session_id,
)
end
end

def threatmetrix_iframe_url(session_id)
source = if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled
Rails.application.routes.url_helpers.test_device_profiling_iframe_url
else
'https://h.online-metrix.net/fp/tags'
end

UriService.add_params(
source,
org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id,
session_id: session_id,
)
end
end
end
end
36 changes: 36 additions & 0 deletions app/services/threat_metrix_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module ThreatMetrixHelper
THREAT_METRIX_URL = 'https://h.online-metrix.net/fp'

# @return [Array<String>]
def threatmetrix_javascript_urls(session_id)
sources = if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled
Rails.application.config.asset_sources.get_sources('mock-device-profiling')
else
["#{THREAT_METRIX_URL}/tags.js"]
end

sources.map do |source|
UriService.add_params(
source,
org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id,
session_id: session_id,
)
end
end

def threatmetrix_iframe_url(session_id)
source = if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled
Rails.application.routes.url_helpers.test_device_profiling_iframe_url
else
"#{THREAT_METRIX_URL}/tags"
end

UriService.add_params(
source,
org_id: IdentityConfig.store.lexisnexis_threatmetrix_org_id,
session_id: session_id,
)
end
end
19 changes: 6 additions & 13 deletions app/views/idv/shared/ssn.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,12 @@ locals:
</p>

<% if FeatureManagement.proofing_device_profiling_collecting_enabled? %>
<% if threatmetrix_session_id.present? %>
<% threatmetrix_javascript_urls.each do |threatmetrix_javascript_url| %>
<%= javascript_include_tag threatmetrix_javascript_url, nonce: true %>
<% end %>
<noscript>
<%= content_tag(
:iframe,
'',
src: threatmetrix_iframe_url,
style: 'width: 100px; height: 100px; border: 0; position: absolute; top: -5000px;',
) %>
</noscript>
<% end %>
<%= render partial: 'shared/threat_metrix_profiling',
locals: {
threatmetrix_session_id:,
threatmetrix_javascript_urls:,
threatmetrix_iframe_url:,
} %>
<% end %>

<% if IdentityConfig.store.proofer_mock_fallback %>
Expand Down
19 changes: 19 additions & 0 deletions app/views/shared/_threat_metrix_profiling.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<%#
locals:
* threatmetrix_session_id: Session id for users threatmetrix session
* threatmetrix_javascript_urls: Url for threatmetrix javascript
* threatmetrix_iframe_url: Iframe as fallback in case javascript fails.
%>
<% if threatmetrix_session_id.present? %>
<% threatmetrix_javascript_urls.each do |threatmetrix_javascript_url| %>
<%= javascript_include_tag threatmetrix_javascript_url, nonce: true %>
<% end %>
<noscript>
<%= content_tag(
:iframe,
'',
src: threatmetrix_iframe_url,
style: 'width: 100px; height: 100px; border: 0; position: absolute; top: -5000px;',
) %>
</noscript>
<% end %>
9 changes: 9 additions & 0 deletions app/views/sign_up/registrations/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@

<%= render PageHeadingComponent.new.with_content(t('headings.create_account_new_users')) %>

<% if FeatureManagement.account_creation_device_profiling_collecting_enabled? %>
<%= render partial: 'shared/threat_metrix_profiling',
locals: {
threatmetrix_session_id:,
threatmetrix_javascript_urls:,
threatmetrix_iframe_url:,
} %>
<% end %>

<%= simple_form_for(@register_user_email_form, url: sign_up_register_path) do |f| %>
<%= render ValidatedFieldComponent.new(
form: f,
Expand Down
2 changes: 2 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ aamva_cert_enabled: true
aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]'
aamva_verification_request_timeout: 5.0
aamva_verification_url: https://example.org:12345/verification/url
account_creation_device_profiling: disabled
account_reset_fraud_user_wait_period_days:
account_reset_token_valid_for_days: 1
account_reset_wait_period_days: 1
Expand Down Expand Up @@ -401,6 +402,7 @@ weekly_auth_funnel_report_config: '[]'
development:
aamva_private_key: 123abc
aamva_public_key: 123abc
account_creation_device_profiling: collect_only
attribute_encryption_key: 2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25
attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]'
check_user_password_compromised_enabled: true
Expand Down
10 changes: 10 additions & 0 deletions lib/feature_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ def self.recaptcha_enterprise?
IdentityConfig.store.recaptcha_enterprise_project_id.present?
end

# Whether we collect device profiling information as part of the account creation process
def self.account_creation_device_profiling_collecting_enabled?
case IdentityConfig.store.account_creation_device_profiling
when :enabled, :collect_only then true
when :disabled then false
else
raise 'Invalid value for account_creation_device_profiling'
end
end

# Whether we collect device profiling information as part of the proofing process.
def self.proofing_device_profiling_collecting_enabled?
case IdentityConfig.store.proofing_device_profiling
Expand Down
5 changes: 5 additions & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def self.store
config.add(:aamva_supported_jurisdictions, type: :json)
config.add(:aamva_verification_request_timeout, type: :float)
config.add(:aamva_verification_url)
config.add(
:account_creation_device_profiling,
type: :symbol,
enum: [:disabled, :collect_only, :enabled],
)
config.add(:account_reset_token_valid_for_days, type: :integer)
config.add(:account_reset_wait_period_days, type: :integer)
config.add(:account_reset_fraud_user_wait_period_days, type: :integer, allow_nil: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

require 'rails_helper'

RSpec.describe Idv::ThreatMetrixConcern, type: :controller do
RSpec.describe ThreatMetrixConcern, type: :controller do
controller ApplicationController do
include Idv::ThreatMetrixConcern
include ThreatMetrixConcern

before_action :override_csp_for_threat_metrix

Expand Down