diff --git a/app/controllers/concerns/ab_testing_concern.rb b/app/controllers/concerns/ab_testing_concern.rb
index 16eb6bb2769..6353360e4d5 100644
--- a/app/controllers/concerns/ab_testing_concern.rb
+++ b/app/controllers/concerns/ab_testing_concern.rb
@@ -4,7 +4,7 @@ module AbTestingConcern
# @param [Symbol] test Name of the test, which should correspond to an A/B test defined in
# # config/initializer/ab_tests.rb.
# @return [Symbol,nil] Bucket to use for the given test, or nil if the test is not active.
- def ab_test_bucket(test_name)
+ def ab_test_bucket(test_name, user: current_user)
test = AbTests.all[test_name]
raise "Unknown A/B test: #{test_name}" unless test
@@ -12,7 +12,7 @@ def ab_test_bucket(test_name)
request:,
service_provider: current_sp&.issuer,
session:,
- user: current_user,
+ user:,
user_session:,
)
end
diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb
index 2ca053235a1..12881a61a5c 100644
--- a/app/controllers/idv/enter_password_controller.rb
+++ b/app/controllers/idv/enter_password_controller.rb
@@ -51,6 +51,7 @@ def create
gpo_verification_pending: idv_session.profile.gpo_verification_pending?,
in_person_verification_pending: idv_session.profile.in_person_verification_pending?,
deactivation_reason: idv_session.profile.deactivation_reason,
+ proofing_workflow_time_in_seconds: idv_session.proofing_workflow_time_in_seconds,
**ab_test_analytics_buckets,
)
Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
@@ -62,6 +63,7 @@ def create
gpo_verification_pending: idv_session.profile.gpo_verification_pending?,
in_person_verification_pending: idv_session.profile.in_person_verification_pending?,
deactivation_reason: idv_session.profile.deactivation_reason,
+ proofing_workflow_time_in_seconds: idv_session.proofing_workflow_time_in_seconds,
**ab_test_analytics_buckets,
)
diff --git a/app/controllers/idv/welcome_controller.rb b/app/controllers/idv/welcome_controller.rb
index c2928c47884..1008336ded2 100644
--- a/app/controllers/idv/welcome_controller.rb
+++ b/app/controllers/idv/welcome_controller.rb
@@ -9,6 +9,7 @@ class WelcomeController < ApplicationController
before_action :confirm_not_rate_limited
def show
+ idv_session.proofing_started_at ||= Time.zone.now.iso8601
analytics.idv_doc_auth_welcome_visited(**analytics_arguments)
Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]).
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index ee9205870ae..f88d1424409 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -9,6 +9,7 @@ class SessionsController < Devise::SessionsController
include Api::CsrfTokenConcern
include ForcedReauthenticationConcern
include NewDeviceConcern
+ include AbTestingConcern
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_signin
@@ -41,7 +42,7 @@ def create
handle_valid_authentication
ensure
handle_invalid_authentication if rate_limit_password_failure && !current_user
- track_authentication_attempt(auth_params[:email])
+ track_authentication_attempt
end
def destroy
@@ -97,13 +98,24 @@ def locked_out_time_remaining
def valid_captcha_result?
return @valid_captcha_result if defined?(@valid_captcha_result)
- @valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit(
- email: auth_params[:email],
+ @valid_captcha_result = recaptcha_form.submit(
recaptcha_token: params.require(:user)[:recaptcha_token],
- device_cookie: cookies[:device],
).success?
end
+ def recaptcha_form
+ @recaptcha_form ||= SignInRecaptchaForm.new(
+ email: auth_params[:email],
+ device_cookie: cookies[:device],
+ ab_test_bucket: ab_test_bucket(:RECAPTCHA_SIGN_IN, user: user_from_params),
+ **recaptcha_form_args,
+ )
+ end
+
+ def captcha_validation_performed?
+ !recaptcha_form.exempt?
+ end
+
def process_failed_captcha
sign_out(:user)
warden.lock!
@@ -140,6 +152,11 @@ def check_user_needs_redirect
end
end
+ def user_from_params
+ return @user_from_params if defined?(@user_from_params)
+ @user_from_params = User.find_with_email(auth_params[:email])
+ end
+
def auth_params
params.require(:user).permit(:email, :password)
end
@@ -169,14 +186,15 @@ def handle_valid_authentication
user_id: current_user.id,
email: auth_params[:email],
)
+ user_session[:captcha_validation_performed_at_sign_in] = captcha_validation_performed?
user_session[:platform_authenticator_available] =
params[:platform_authenticator_available] == 'true'
check_password_compromised
redirect_to next_url_after_valid_authentication
end
- def track_authentication_attempt(email)
- user = User.find_with_email(email) || AnonymousUser.new
+ def track_authentication_attempt
+ user = user_from_params || AnonymousUser.new
success = current_user.present? && !user_locked_out?(user) && valid_captcha_result?
analytics.email_and_password_auth(
@@ -184,6 +202,7 @@ def track_authentication_attempt(email)
user_id: user.uuid,
user_locked_out: user_locked_out?(user),
rate_limited: rate_limited?,
+ captcha_validation_performed: captcha_validation_performed?,
valid_captcha_result: valid_captcha_result?,
bad_password_count: session[:bad_password_count].to_i,
sp_request_url_present: sp_session[:request_url].present?,
@@ -202,7 +221,7 @@ def rate_limited?
def rate_limiter
return @rate_limiter if defined?(@rate_limiter)
- user = User.find_with_email(auth_params[:email])
+ user = user_from_params
return @rate_limiter = nil unless user
@rate_limiter = RateLimiter.new(
rate_limit_type: :sign_in_user_id_per_ip,
diff --git a/app/forms/sign_in_recaptcha_form.rb b/app/forms/sign_in_recaptcha_form.rb
index 206e742ea70..5fe5605074f 100644
--- a/app/forms/sign_in_recaptcha_form.rb
+++ b/app/forms/sign_in_recaptcha_form.rb
@@ -5,24 +5,37 @@ class SignInRecaptchaForm
RECAPTCHA_ACTION = 'sign_in'
- attr_reader :form_class, :form_args, :email, :recaptcha_token, :device_cookie
+ attr_reader :form_class, :form_args, :email, :recaptcha_token, :device_cookie, :ab_test_bucket
validate :validate_recaptcha_result
- def initialize(form_class: RecaptchaForm, **form_args)
+ def initialize(
+ email:,
+ device_cookie:,
+ ab_test_bucket:,
+ form_class: RecaptchaForm,
+ **form_args
+ )
+ @email = email
+ @device_cookie = device_cookie
+ @ab_test_bucket = ab_test_bucket
@form_class = form_class
@form_args = form_args
end
- def submit(email:, recaptcha_token:, device_cookie:)
- @email = email
+ def submit(recaptcha_token:)
@recaptcha_token = recaptcha_token
- @device_cookie = device_cookie
success = valid?
FormResponse.new(success:, errors:, serialize_error_details_only: true)
end
+ def exempt?
+ IdentityConfig.store.sign_in_recaptcha_score_threshold.zero? ||
+ ab_test_bucket != :sign_in_recaptcha ||
+ device.present?
+ end
+
private
def validate_recaptcha_result
@@ -35,7 +48,7 @@ def device
end
def score_threshold
- if IdentityConfig.store.sign_in_recaptcha_score_threshold.zero? || device.present?
+ if exempt?
0.0
else
IdentityConfig.store.sign_in_recaptcha_score_threshold
diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb
index 7148c980d33..297b6fa71c1 100644
--- a/app/forms/webauthn_setup_form.rb
+++ b/app/forms/webauthn_setup_form.rb
@@ -61,7 +61,7 @@ def generic_error_message
private
- attr_reader :success, :transports, :invalid_transports, :protocol
+ attr_reader :success, :transports, :aaguid, :invalid_transports, :protocol
attr_accessor :user, :challenge, :attestation_object, :client_data_json,
:name, :platform_authenticator, :authenticator_data_flags, :device_name
@@ -110,6 +110,7 @@ def valid_attestation_response?(protocol)
)
begin
+ @aaguid = attestation_response.authenticator_data.aaguid
attestation_response.valid?(@challenge.pack('c*'), original_origin)
rescue StandardError
false
@@ -141,6 +142,7 @@ def create_webauthn_configuration
platform_authenticator: platform_authenticator,
transports: transports.presence,
authenticator_data_flags: authenticator_data_flags,
+ aaguid: aaguid,
)
end
@@ -172,6 +174,7 @@ def extra_analytics_attributes
pii_like_keypaths: [[:mfa_method_counts, :phone]],
authenticator_data_flags: authenticator_data_flags,
unknown_transports: invalid_transports.presence,
+ aaguid: aaguid,
}.compact
end
end
diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb
index 9dec32f69fe..686770007ff 100644
--- a/app/forms/webauthn_verification_form.rb
+++ b/app/forms/webauthn_verification_form.rb
@@ -92,6 +92,7 @@ def valid_assertion_response?
client_data_json.blank? ||
signature.blank? ||
challenge.blank?
+
WebAuthn::AuthenticatorAssertionResponse.new(
authenticator_data: Base64.decode64(authenticator_data),
client_data_json: Base64.decode64(client_data_json),
@@ -165,6 +166,7 @@ def extra_analytics_attributes
{
webauthn_configuration_id: webauthn_configuration&.id,
frontend_error: webauthn_error.presence,
+ webauthn_aaguid: webauthn_configuration&.aaguid,
}.compact
end
end
diff --git a/app/jobs/reports/fraud_metrics_report.rb b/app/jobs/reports/fraud_metrics_report.rb
index 33aa19c5685..704d9beb6fd 100644
--- a/app/jobs/reports/fraud_metrics_report.rb
+++ b/app/jobs/reports/fraud_metrics_report.rb
@@ -29,7 +29,7 @@ def perform(date = Time.zone.yesterday.end_of_day)
ReportMailer.tables_report(
email: email_addresses,
- subject: "Fraud Metrics Report - #{date.to_date}",
+ subject: "Fraud Metrics Report - #{report_date.to_date}",
reports: reports,
message: preamble,
attachment_format: :xlsx,
@@ -60,9 +60,7 @@ def preamble(env: Identity::Hostdata.env || 'local')
end
def reports
- @reports ||= [
- fraud_metrics_lg99_report.as_emailable_reports,
- ]
+ @reports ||= fraud_metrics_lg99_report.as_emailable_reports
end
def fraud_metrics_lg99_report
diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb
index a0959d4f7bc..44401a8914e 100644
--- a/app/jobs/resolution_proofing_job.rb
+++ b/app/jobs/resolution_proofing_job.rb
@@ -73,6 +73,16 @@ def perform(
device_profiling_success: callback_log_data&.device_profiling_success,
timing: timer.results,
)
+
+ if IdentityConfig.store.idv_socure_shadow_mode_enabled
+ SocureShadowModeProofingJob.perform_later(
+ document_capture_session_result_id: document_capture_session.result_id,
+ encrypted_arguments:,
+ service_provider_issuer:,
+ user_email: user_email_for_proofing(user),
+ user_uuid: user.uuid,
+ )
+ end
end
private
@@ -89,7 +99,7 @@ def make_vendor_proofing_requests(
)
result = progressive_proofer.proof(
applicant_pii: applicant_pii,
- user_email: user.confirmed_email_addresses.first.email,
+ user_email: user_email_for_proofing(user),
threatmetrix_session_id: threatmetrix_session_id,
request_ip: request_ip,
ipp_enrollment_in_progress: ipp_enrollment_in_progress,
@@ -109,6 +119,10 @@ def make_vendor_proofing_requests(
)
end
+ def user_email_for_proofing(user)
+ user.confirmed_email_addresses.first.email
+ end
+
def log_threatmetrix_info(threatmetrix_result, user)
logger_info_hash(
name: 'ThreatMetrix',
diff --git a/app/jobs/socure_shadow_mode_proofing_job.rb b/app/jobs/socure_shadow_mode_proofing_job.rb
new file mode 100644
index 00000000000..fc39a3280c2
--- /dev/null
+++ b/app/jobs/socure_shadow_mode_proofing_job.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+class SocureShadowModeProofingJob < ApplicationJob
+ include JobHelpers::StaleJobHelper
+
+ queue_as :low
+
+ discard_on JobHelpers::StaleJobHelper::StaleJobError
+
+ # @param [String] document_capture_session_result_id
+ # @param [String] encrypted_arguments
+ # @param [String,nil] service_provider_issuer
+ # @param [String] user_email
+ # @param [String] user_uuid
+ def perform(
+ document_capture_session_result_id:,
+ encrypted_arguments:,
+ service_provider_issuer:,
+ user_email:,
+ user_uuid:
+ )
+ raise_stale_job! if stale_job?(enqueued_at)
+
+ user = User.find_by(uuid: user_uuid)
+ raise "User not found: #{user_uuid}" if !user
+
+ analytics = create_analytics(
+ user:,
+ service_provider_issuer:,
+ )
+
+ proofing_result = load_proofing_result(document_capture_session_result_id:)
+ if !proofing_result
+ analytics.idv_socure_shadow_mode_proofing_result_missing
+ return
+ end
+
+ applicant = build_applicant(encrypted_arguments:, user_email:)
+
+ socure_result = proofer.proof(applicant)
+
+ analytics.idv_socure_shadow_mode_proofing_result(
+ resolution_result: format_proofing_result_for_logs(proofing_result),
+ socure_result: socure_result.to_h,
+ user_id: user.uuid,
+ pii_like_keypaths: [
+ [:errors, :ssn],
+ [:resolution_result, :context, :stages, :resolution, :errors, :ssn],
+ [:resolution_result, :context, :stages, :residential_address, :errors, :ssn],
+ [:resolution_result, :context, :stages, :threatmetrix, :response_body, :first_name],
+ [:resolution_result, :context, :stages, :state_id, :state_id_jurisdiction],
+ ],
+ )
+ end
+
+ def create_analytics(
+ user:,
+ service_provider_issuer:
+ )
+ Analytics.new(
+ user:,
+ request: nil,
+ sp: service_provider_issuer,
+ session: {},
+ )
+ end
+
+ def format_proofing_result_for_logs(proofing_result)
+ proofing_result.to_h.tap do |hash|
+ hash.dig(:context, :stages, :threatmetrix)&.delete(:response_body)
+ end
+ end
+
+ def load_proofing_result(document_capture_session_result_id:)
+ DocumentCaptureSession.new(
+ result_id: document_capture_session_result_id,
+ ).load_proofing_result&.result
+ end
+
+ def build_applicant(
+ encrypted_arguments:,
+ user_email:
+ )
+ decrypted_arguments = JSON.parse(
+ Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(encrypted_arguments),
+ symbolize_names: true,
+ )
+
+ applicant_pii = decrypted_arguments[:applicant_pii]
+
+ {
+ **applicant_pii.slice(
+ :first_name,
+ :last_name,
+ :address1,
+ :address2,
+ :city,
+ :state,
+ :zipcode,
+ :phone,
+ :dob,
+ :ssn,
+ ),
+ email: user_email,
+ }
+ end
+
+ def proofer
+ @proofer ||= Proofing::Socure::IdPlus::Proofer.new(
+ Proofing::Socure::IdPlus::Config.new(
+ api_key: IdentityConfig.store.socure_idplus_api_key,
+ base_url: IdentityConfig.store.socure_idplus_base_url,
+ timeout: IdentityConfig.store.socure_idplus_timeout_in_seconds,
+ ),
+ )
+ end
+end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index d1a2b002959..40ff7c31086 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -430,7 +430,8 @@ def edit_password_visit(required_password_change: false, **extra)
# @param [String] user_id
# @param [Boolean] user_locked_out if the user is currently locked out of their second factor
# @param [Boolean] rate_limited Whether the user has exceeded user IP rate limiting
- # @param [Boolean] valid_captcha_result Whether user passed the reCAPTCHA check
+ # @param [Boolean] valid_captcha_result Whether user passed the reCAPTCHA check or was exempt
+ # @param [Boolean] captcha_validation_performed Whether a reCAPTCHA check was performed
# @param [String] bad_password_count represents number of prior login failures
# @param [Boolean] sp_request_url_present if was an SP request URL in the session
# @param [Boolean] remember_device if the remember device cookie was present
@@ -443,6 +444,7 @@ def email_and_password_auth(
user_locked_out:,
rate_limited:,
valid_captcha_result:,
+ captcha_validation_performed:,
bad_password_count:,
sp_request_url_present:,
remember_device:,
@@ -456,6 +458,7 @@ def email_and_password_auth(
user_locked_out:,
rate_limited:,
valid_captcha_result:,
+ captcha_validation_performed:,
bad_password_count:,
sp_request_url_present:,
remember_device:,
@@ -1858,6 +1861,7 @@ def idv_doc_auth_welcome_visited(step:, analytics_id:, skip_hybrid_handoff: nil,
# @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any.
# @param [String,nil] active_profile_idv_level ID verification level of user's active profile.
# @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile.
+ # @param [Integer,nil] proofing_workflow_time_in_seconds The time since starting proofing
# @identity.idp.previous_event_name IdV: review info visited
def idv_enter_password_submitted(
success:,
@@ -1871,6 +1875,7 @@ def idv_enter_password_submitted(
proofing_components: nil,
active_profile_idv_level: nil,
pending_profile_idv_level: nil,
+ proofing_workflow_time_in_seconds: nil,
**extra
)
track_event(
@@ -1886,6 +1891,7 @@ def idv_enter_password_submitted(
proofing_components:,
active_profile_idv_level:,
pending_profile_idv_level:,
+ proofing_workflow_time_in_seconds:,
**extra,
)
end
@@ -1946,6 +1952,7 @@ def idv_enter_password_visited(
# @param [String,nil] active_profile_idv_level ID verification level of user's active profile.
# @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile.
# @param [Array,nil] profile_history Array of user's profiles (oldest to newest).
+ # @param [Integer,nil] proofing_workflow_time_in_seconds The time since starting proofing
# @see Reporting::IdentityVerificationReport#query This event is used by the identity verification
# report. Changes here should be reflected there.
# Tracks the last step of IDV, indicates the user successfully proofed
@@ -1962,6 +1969,7 @@ def idv_final(
active_profile_idv_level: nil,
pending_profile_idv_level: nil,
profile_history: nil,
+ proofing_workflow_time_in_seconds: nil,
**extra
)
track_event(
@@ -1978,6 +1986,7 @@ def idv_final(
active_profile_idv_level:,
pending_profile_idv_level:,
profile_history:,
+ proofing_workflow_time_in_seconds:,
**extra,
)
end
@@ -4195,6 +4204,28 @@ def idv_session_error_visited(
)
end
+ # Logs a Socure KYC result alongside a resolution result for later comparison.
+ # @param [Hash] socure_result Result from Socure KYC API call
+ # @param [Hash] resolution_result Result from resolution proofing
+ def idv_socure_shadow_mode_proofing_result(
+ socure_result:,
+ resolution_result:,
+ **extra
+ )
+ track_event(
+ :idv_socure_shadow_mode_proofing_result,
+ resolution_result: resolution_result.to_h,
+ socure_result: socure_result.to_h,
+ **extra,
+ )
+ end
+
+ # Indicates that no proofing result was found when SocureShadowModeProofingJob
+ # attempted to look for one.
+ def idv_socure_shadow_mode_proofing_result_missing(**extra)
+ track_event(:idv_socure_shadow_mode_proofing_result_missing, **extra)
+ end
+
# @param [String] step
# @param [String] location
# @param [Hash,nil] proofing_components User's current proofing components
diff --git a/app/services/encryption/contextless_kms_client.rb b/app/services/encryption/contextless_kms_client.rb
index 3500b968fcb..392f9d0e34c 100644
--- a/app/services/encryption/contextless_kms_client.rb
+++ b/app/services/encryption/contextless_kms_client.rb
@@ -18,14 +18,14 @@ class ContextlessKmsClient
KMS: 'KMSx',
}.freeze
- def encrypt(plaintext)
- KmsLogger.log(:encrypt, key_id: IdentityConfig.store.aws_kms_key_id)
+ def encrypt(plaintext, log_context: nil)
+ KmsLogger.log(:encrypt, key_id: IdentityConfig.store.aws_kms_key_id, log_context: log_context)
return encrypt_kms(plaintext) if FeatureManagement.use_kms?
encrypt_local(plaintext)
end
- def decrypt(ciphertext)
- KmsLogger.log(:decrypt, key_id: IdentityConfig.store.aws_kms_key_id)
+ def decrypt(ciphertext, log_context: nil)
+ KmsLogger.log(:decrypt, key_id: IdentityConfig.store.aws_kms_key_id, log_context: log_context)
return decrypt_kms(ciphertext) if use_kms?(ciphertext)
decrypt_local(ciphertext)
end
diff --git a/app/services/encryption/kms_client.rb b/app/services/encryption/kms_client.rb
index 308606a7d1c..55d44d24cd5 100644
--- a/app/services/encryption/kms_client.rb
+++ b/app/services/encryption/kms_client.rb
@@ -38,7 +38,9 @@ def encrypt(plaintext, encryption_context)
end
def decrypt(ciphertext, encryption_context)
- return decrypt_contextless_kms(ciphertext) if self.class.looks_like_contextless?(ciphertext)
+ if self.class.looks_like_contextless?(ciphertext)
+ return decrypt_contextless_kms(ciphertext, encryption_context)
+ end
KmsLogger.log(:decrypt, context: encryption_context, key_id: kms_key_id)
return decrypt_kms(ciphertext, encryption_context) if use_kms?(ciphertext)
decrypt_local(ciphertext, encryption_context)
@@ -135,8 +137,8 @@ def local_encryption_key(encryption_context)
)
end
- def decrypt_contextless_kms(ciphertext)
- ContextlessKmsClient.new.decrypt(ciphertext)
+ def decrypt_contextless_kms(ciphertext, encryption_context)
+ ContextlessKmsClient.new.decrypt(ciphertext, log_context: encryption_context)
end
# chunk plaintext into ~4096 byte chunks, but not less than 1024 bytes in a chunk if chunking.
diff --git a/app/services/encryption/kms_logger.rb b/app/services/encryption/kms_logger.rb
index 4276f9d2e98..9824a4ef9ff 100644
--- a/app/services/encryption/kms_logger.rb
+++ b/app/services/encryption/kms_logger.rb
@@ -2,11 +2,12 @@
module Encryption
class KmsLogger
- def self.log(action, key_id:, context: nil)
+ def self.log(action, key_id:, context: nil, log_context: nil)
output = {
kms: {
action: action,
encryption_context: context,
+ log_context: log_context,
key_id: key_id,
},
log_filename: Idp::Constants::KMS_LOG_FILENAME,
diff --git a/app/services/encryption/password_verifier.rb b/app/services/encryption/password_verifier.rb
index 95f0a52d6e9..7e5e80fc05d 100644
--- a/app/services/encryption/password_verifier.rb
+++ b/app/services/encryption/password_verifier.rb
@@ -73,7 +73,7 @@ def create_digest_pair(password:, user_uuid:)
def verify(password:, digest_pair:, user_uuid:)
digest = digest_pair.multi_or_single_region_ciphertext
password_digest = PasswordDigest.parse_from_string(digest)
- return verify_uak_digest(password, digest) if stale_digest?(digest)
+ return verify_uak_digest(password, digest) if password_digest.uak_password_digest?
verify_password_against_digest(
password: password,
diff --git a/app/services/encryption/user_access_key.rb b/app/services/encryption/user_access_key.rb
index f3b9363c7e5..bf0887648f8 100644
--- a/app/services/encryption/user_access_key.rb
+++ b/app/services/encryption/user_access_key.rb
@@ -45,7 +45,7 @@ def unlock(encryption_key_arg)
self.masked_ciphertext = Base64.strict_decode64(encryption_key_arg)
z1_padded = z1.dup.rjust(masked_ciphertext.length, '0')
encrypted_random_r = xor(z1_padded, masked_ciphertext)
- self.random_r = kms_client.decrypt(encrypted_random_r)
+ self.random_r = kms_client.decrypt(encrypted_random_r, log_context: 'user-access-key')
self.cek = OpenSSL::Digest::SHA256.hexdigest(z2 + random_r)
self
end
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index 9e76d23f2d6..e1c12d0e067 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -22,6 +22,7 @@ class Session
phone_for_mobile_flow
previous_phone_step_params
profile_id
+ proofing_started_at
redo_document_capture
resolution_successful
selfie_check_performed
@@ -205,6 +206,10 @@ def add_failed_phone_step_number(phone)
failed_phone_step_numbers << phone_e164 if !failed_phone_step_numbers.include?(phone_e164)
end
+ def proofing_workflow_time_in_seconds
+ Time.zone.now - Time.zone.parse(proofing_started_at) if proofing_started_at.present?
+ end
+
def pii_from_user_in_flow_session
user_session.dig('idv/in_person', :pii_from_user)
end
diff --git a/app/services/proofing/resolution/result.rb b/app/services/proofing/resolution/result.rb
index f8d07ea6398..0978ce995bb 100644
--- a/app/services/proofing/resolution/result.rb
+++ b/app/services/proofing/resolution/result.rb
@@ -61,6 +61,7 @@ def to_h
attributes_requiring_additional_verification,
vendor_name: vendor_name,
vendor_workflow: vendor_workflow,
+ verified_attributes: verified_attributes,
}
end
diff --git a/app/views/users/service_provider_revoke/show.html.erb b/app/views/users/service_provider_revoke/show.html.erb
index b68e5f2c448..ddd83eef0ce 100644
--- a/app/views/users/service_provider_revoke/show.html.erb
+++ b/app/views/users/service_provider_revoke/show.html.erb
@@ -11,8 +11,6 @@
<%= button_to(service_provider_revoke_url(@service_provider.id), method: 'delete', class: 'usa-button usa-button--wide usa-button--big margin-top-4') { t('forms.buttons.continue') } %>
-
-
- <%= link_to(t('links.cancel'), account_url) %>
-
-
+<%= render PageFooterComponent.new do %>
+ <%= link_to(t('links.cancel'), account_connected_accounts_path) %>
+<% end %>
diff --git a/config/application.yml.default b/config/application.yml.default
index 039fd0d3300..f2993cb3feb 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -143,6 +143,7 @@ idv_max_attempts: 5
idv_min_age_years: 13
idv_send_link_attempt_window_in_minutes: 10
idv_send_link_max_attempts: 5
+idv_socure_shadow_mode_enabled: false
idv_sp_required: false
in_person_completion_survey_url: 'https://login.gov'
in_person_doc_auth_button_enabled: true
@@ -332,11 +333,15 @@ short_term_phone_otp_max_attempt_window_in_seconds: 10
short_term_phone_otp_max_attempts: 2
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
sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1
sign_in_user_id_per_ip_attempt_window_in_minutes: 720
sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200
sign_in_user_id_per_ip_max_attempts: 50
+socure_idplus_api_key: ''
+socure_idplus_base_url: ''
+socure_idplus_timeout_in_seconds: 5
socure_webhook_enabled: false
socure_webhook_secret_key: ''
socure_webhook_secret_key_queue: '[]'
@@ -428,10 +433,10 @@ development:
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"]'
- socure_webhook_secret_key: 'secret-key'
- socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]'
+ socure_idplus_base_url: 'https://sandbox.socure.us'
state_tracking_enabled: true
telephony_adapter: test
use_dashboard_service_providers: true
diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb
index 70fdddd95c2..7150e1d1e14 100644
--- a/config/initializers/ab_tests.rb
+++ b/config/initializers/ab_tests.rb
@@ -62,4 +62,21 @@ def self.all
) do |service_provider:, session:, user:, user_session:, **|
document_capture_session_uuid_discriminator(service_provider:, session:, user:, user_session:)
end.freeze
+
+ RECAPTCHA_SIGN_IN = AbTest.new(
+ experiment_name: 'reCAPTCHA at Sign-In',
+ should_log: [
+ 'Email and Password Authentication',
+ 'IdV: doc auth verify proofing results',
+ 'reCAPTCHA verify result received',
+ :idv_enter_password_submitted,
+ ].to_set,
+ buckets: { sign_in_recaptcha: IdentityConfig.store.sign_in_recaptcha_percent_tested },
+ ) do |user:, user_session:, **|
+ if user_session&.[](:captcha_validation_performed_at_sign_in) == false
+ nil
+ else
+ user&.uuid
+ end
+ end.freeze
end
diff --git a/lib/ab_test.rb b/lib/ab_test.rb
index 840cd1cc828..1cc266c215f 100644
--- a/lib/ab_test.rb
+++ b/lib/ab_test.rb
@@ -60,6 +60,8 @@ def bucket(request:, service_provider:, session:, user:, user_session:)
def include_in_analytics_event?(event_name)
if should_log.is_a?(Regexp)
should_log.match?(event_name)
+ elsif should_log.respond_to?(:include?)
+ should_log.include?(event_name)
elsif !should_log.nil?
raise 'Unexpected value used for should_log'
else
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index cfa409bbd2f..d4af025ba85 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -165,6 +165,7 @@ def self.store
config.add(:idv_min_age_years, type: :integer)
config.add(:idv_send_link_attempt_window_in_minutes, type: :integer)
config.add(:idv_send_link_max_attempts, type: :integer)
+ config.add(:idv_socure_shadow_mode_enabled, type: :boolean)
config.add(:idv_sp_required, type: :boolean)
config.add(:in_person_completion_survey_url, type: :string)
config.add(:in_person_doc_auth_button_enabled, type: :boolean)
@@ -360,6 +361,9 @@ def self.store
config.add(:s3_reports_enabled, type: :boolean)
config.add(:saml_endpoint_configs, type: :json, options: { symbolize_names: true })
config.add(:saml_secret_rotation_enabled, type: :boolean)
+ config.add(:socure_idplus_api_key, type: :string)
+ config.add(:socure_idplus_base_url, type: :string)
+ config.add(:socure_idplus_timeout_in_seconds, type: :integer)
config.add(:scrypt_cost, type: :string)
config.add(:second_mfa_reminder_account_age_in_days, type: :integer)
config.add(:second_mfa_reminder_sign_in_count, type: :integer)
@@ -382,6 +386,7 @@ def self.store
config.add(:sign_in_user_id_per_ip_attempt_window_in_minutes, type: :integer)
config.add(:sign_in_user_id_per_ip_attempt_window_max_minutes, type: :integer)
config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer)
+ 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(:socure_webhook_enabled, type: :boolean)
diff --git a/lib/reporting/fraud_metrics_lg99_report.rb b/lib/reporting/fraud_metrics_lg99_report.rb
index b3c26f5d4ae..fbcad448f23 100644
--- a/lib/reporting/fraud_metrics_lg99_report.rb
+++ b/lib/reporting/fraud_metrics_lg99_report.rb
@@ -51,22 +51,30 @@ def progress?
end
def as_emailable_reports
- Reporting::EmailableReport.new(
- title: 'LG-99 Metrics',
- table: lg99_metrics_table,
- filename: 'lg99_metrics',
- )
+ [
+ Reporting::EmailableReport.new(
+ title: "Monthly LG-99 Metrics #{stats_month}",
+ table: lg99_metrics_table,
+ filename: 'lg99_metrics',
+ ),
+ Reporting::EmailableReport.new(
+ title: "Monthly Suspended User Metrics #{stats_month}",
+ table: suspended_metrics_table,
+ filename: 'suspended_metrics',
+ ),
+ Reporting::EmailableReport.new(
+ title: "Monthly Reinstated User Metrics #{stats_month}",
+ table: reinstated_metrics_table,
+ filename: 'reinstated_metrics',
+ ),
+ ]
end
def lg99_metrics_table
[
- ['Metric', 'Total'],
- ['Unique users seeing LG-99', lg99_unique_users_count.to_s],
- ['Unique users suspended', unique_suspended_users_count.to_s],
- ['Average Days Creation to Suspension', user_days_to_suspension_avg.to_s],
- ['Average Days Proofed to Suspension', user_days_proofed_to_suspension_avg.to_s],
- ['Unique users reinstated', unique_reinstated_users_count.to_s],
- ['Average Days to Reinstatement', user_days_to_reinstatement_avg.to_s],
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users seeing LG-99', lg99_unique_users_count.to_s, time_range.begin.to_s,
+ time_range.end.to_s],
]
rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err
[
@@ -75,12 +83,50 @@ def lg99_metrics_table
]
end
- def to_csv
- CSV.generate do |csv|
- lg99_metrics_table.each do |row|
- csv << row
- end
- end
+ def suspended_metrics_table
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ [
+ 'Unique users suspended',
+ unique_suspended_users_count.to_s,
+ time_range.begin.to_s,
+ time_range.end.to_s,
+ ],
+ [
+ 'Average Days Creation to Suspension',
+ user_days_to_suspension_avg.to_s,
+ time_range.begin.to_s,
+ time_range.end.to_s,
+ ],
+ [
+ 'Average Days Proofed to Suspension',
+ user_days_proofed_to_suspension_avg.to_s,
+ time_range.begin.to_s,
+ time_range.end.to_s,
+ ],
+ ]
+ end
+
+ def reinstated_metrics_table
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ [
+ 'Unique users reinstated',
+ unique_reinstated_users_count.to_s,
+ time_range.begin.to_s,
+ time_range.end.to_s,
+ ],
+ [
+ 'Average Days to Reinstatement',
+ user_days_to_reinstatement_avg.to_s,
+ time_range.begin.to_s,
+ time_range.end.to_s,
+ ],
+ ]
+ end
+
+ def stats_month
+ time_range.begin.strftime('%b-%Y')
end
# event name => set(user ids)
diff --git a/package.json b/package.json
index 521c35d6c8d..a95a3c5ac8a 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"cleave.js": "^1.6.0",
"concurrently": "^8.2.2",
"core-js": "^3.21.1",
- "fast-glob": "^3.2.7",
+ "fast-glob": "^3.3.2",
"foundation-emails": "^2.3.1",
"intl-tel-input": "^17.0.19",
"react": "^17.0.2",
diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb
index f858344346d..975e12b1c37 100644
--- a/spec/config/initializers/ab_tests_spec.rb
+++ b/spec/config/initializers/ab_tests_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe AbTests do
+ include AbTestsHelper
+
describe '#all' do
it 'returns all registered A/B tests' do
expect(AbTests.all.values).to all(be_kind_of(AbTest))
@@ -157,10 +159,64 @@
it_behaves_like 'an A/B test that uses document_capture_session_uuid as a discriminator'
end
- def reload_ab_tests
- AbTests.all.each do |(name, _)|
- AbTests.send(:remove_const, name)
+ describe 'RECAPTCHA_SIGN_IN' do
+ let(:user) { nil }
+ let(:user_session) { {} }
+
+ subject(:bucket) do
+ AbTests::RECAPTCHA_SIGN_IN.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(:sign_in_recaptcha_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(:sign_in_recaptcha_percent_tested).and_return(100)
+ reload_ab_tests
+ end
+
+ context 'with no associated user' do
+ let(:user) { nil }
+
+ it 'does not return a bucket' do
+ expect(bucket).to be_nil
+ end
+ end
+
+ context 'with an associated user' do
+ let(:user) { build(:user) }
+
+ it 'returns a bucket' do
+ expect(bucket).not_to be_nil
+ end
+
+ context 'with user session indicating recaptcha was not performed at sign-in' do
+ let(:user_session) { { captcha_validation_performed_at_sign_in: false } }
+
+ it 'does not return a bucket' do
+ expect(bucket).to be_nil
+ end
+ end
+ end
end
- load('config/initializers/ab_tests.rb')
end
end
diff --git a/spec/controllers/concerns/ab_testing_concern_spec.rb b/spec/controllers/concerns/ab_testing_concern_spec.rb
index 94898cd827e..cb5e546a064 100644
--- a/spec/controllers/concerns/ab_testing_concern_spec.rb
+++ b/spec/controllers/concerns/ab_testing_concern_spec.rb
@@ -68,5 +68,21 @@
end.to raise_error RuntimeError, 'Unknown A/B test: NOT_A_REAL_TEST'
end
end
+
+ context 'with user keyword argument' do
+ let(:other_user) { build(:user) }
+
+ it 'returns bucket determined using given user' do
+ expect(ab_test).to receive(:bucket).with(
+ user: other_user,
+ request:,
+ service_provider: service_provider.issuer,
+ session:,
+ user_session:,
+ ).and_call_original
+
+ subject.ab_test_bucket(:TEST_TEST, user: other_user)
+ end
+ end
end
end
diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb
index e4cf07dc023..a8e760ce11e 100644
--- a/spec/controllers/idv/enter_password_controller_spec.rb
+++ b/spec/controllers/idv/enter_password_controller_spec.rb
@@ -27,6 +27,7 @@
allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false)
allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args)
subject.idv_session.welcome_visited = true
+ subject.idv_session.proofing_started_at = 5.minutes.ago.iso8601
subject.idv_session.idv_consent_given = true
subject.idv_session.flow_path = 'standard'
subject.idv_session.pii_from_doc = Pii::StateId.new(**Idp::Constants::MOCK_IDV_APPLICANT)
@@ -283,7 +284,7 @@ def show
end
end
- it 'redirects to personal key path' do
+ it 'redirects to personal key path', :freeze_time do
put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } }
expect(@analytics).to have_logged_event(
@@ -294,6 +295,7 @@ def show
fraud_rejection: false,
gpo_verification_pending: false,
in_person_verification_pending: false,
+ proofing_workflow_time_in_seconds: 5.minutes.to_i,
**ab_test_args,
),
)
diff --git a/spec/controllers/idv/welcome_controller_spec.rb b/spec/controllers/idv/welcome_controller_spec.rb
index bd32fefbc61..269632a3177 100644
--- a/spec/controllers/idv/welcome_controller_spec.rb
+++ b/spec/controllers/idv/welcome_controller_spec.rb
@@ -64,15 +64,30 @@
)
end
+ it 'sets the proofing started timestamp', :freeze_time do
+ get :show
+
+ expect(subject.idv_session.proofing_started_at).to eq(Time.zone.now.iso8601)
+ end
+
context 'welcome already visited' do
- it 'does not redirect to agreement' do
+ before do
subject.idv_session.welcome_visited = true
+ subject.idv_session.proofing_started_at = 5.minutes.ago.iso8601
+ end
+ it 'does not redirect to agreement' do
get :show
expect(response).to render_template('idv/welcome/show')
end
+ it 'does not overwrite the proofing started timestamp' do
+ get :show
+
+ expect(subject.idv_session.proofing_started_at).to eq(5.minutes.ago.iso8601)
+ end
+
context 'and verify info already completed' do
before do
subject.idv_session.flow_path = 'standard'
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index 387df9187b0..9d9a42732f2 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -3,6 +3,7 @@
RSpec.describe Users::SessionsController, devise: true do
include ActionView::Helpers::DateHelper
include ActionView::Helpers::UrlHelper
+ include AbTestsHelper
let(:mock_valid_site) { 'http://example.com' }
@@ -55,6 +56,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: false,
@@ -128,6 +130,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: false,
@@ -211,6 +214,7 @@
user_locked_out: false,
rate_limited: true,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 8,
sp_request_url_present: false,
remember_device: false,
@@ -233,6 +237,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 1,
sp_request_url_present: false,
remember_device: false,
@@ -253,6 +258,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 1,
sp_request_url_present: false,
remember_device: false,
@@ -277,45 +283,50 @@
user_locked_out: true,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: false,
)
end
- it 'tracks unsuccessful authentication for failed reCAPTCHA' do
- user = create(:user, :fully_registered)
+ context 'with reCAPTCHA validation enabled' do
+ before do
+ allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true)
+ allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true)
+ allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2)
+ allow(controller).to receive(:ab_test_bucket).with(:RECAPTCHA_SIGN_IN, kind_of(Hash)).
+ and_return(:sign_in_recaptcha)
+ end
- allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true)
- allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true)
- allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2)
- stub_analytics
+ it 'tracks unsuccessful authentication for failed reCAPTCHA' do
+ user = create(:user, :fully_registered)
- post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+ stub_analytics
- expect(@analytics).to have_logged_event(
- 'Email and Password Authentication',
- success: false,
- user_id: user.uuid,
- user_locked_out: false,
- rate_limited: false,
- valid_captcha_result: false,
- bad_password_count: 0,
- remember_device: false,
- sp_request_url_present: false,
- )
- end
+ post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
- it 'redirects unsuccessful authentication for failed reCAPTCHA to failed page' do
- user = create(:user, :fully_registered)
+ expect(@analytics).to have_logged_event(
+ 'Email and Password Authentication',
+ success: false,
+ user_id: user.uuid,
+ user_locked_out: false,
+ rate_limited: false,
+ valid_captcha_result: false,
+ captcha_validation_performed: true,
+ bad_password_count: 0,
+ remember_device: false,
+ sp_request_url_present: false,
+ )
+ end
- allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true)
- allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true)
- allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2)
+ it 'redirects unsuccessful authentication for failed reCAPTCHA to failed page' do
+ user = create(:user, :fully_registered)
- post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
+ post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } }
- expect(response).to redirect_to sign_in_security_check_failed_url
+ expect(response).to redirect_to sign_in_security_check_failed_url
+ end
end
it 'tracks count of multiple unsuccessful authentication attempts' do
@@ -335,6 +346,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 2,
sp_request_url_present: false,
remember_device: false,
@@ -354,6 +366,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 1,
sp_request_url_present: true,
remember_device: false,
@@ -525,6 +538,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: false,
@@ -650,6 +664,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: true,
@@ -677,6 +692,7 @@
user_locked_out: false,
rate_limited: false,
valid_captcha_result: true,
+ captcha_validation_performed: false,
bad_password_count: 0,
sp_request_url_present: false,
remember_device: true,
diff --git a/spec/features/account_connected_apps_spec.rb b/spec/features/account_connected_apps_spec.rb
index 7588b9dac78..0674a4c3ac8 100644
--- a/spec/features/account_connected_apps_spec.rb
+++ b/spec/features/account_connected_apps_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe 'Account connected applications' do
+ include NavigationHelper
+
let(:user) { create(:user, :fully_registered, created_at: Time.zone.now - 100.days) }
let(:identity_with_link) do
create(
@@ -30,13 +32,15 @@
before do
sign_in_and_2fa_user(user)
build_account_connected_apps
- visit account_connected_accounts_path
+ within_sidenav { click_on t('account.navigation.connected_accounts') }
end
scenario 'viewing account connected applications' do
expect(page).to have_content(t('headings.account.connected_accounts'))
- visit account_history_path
+ expect(identity_without_link_timestamp).to appear_before(identity_with_link_timestamp)
+
+ within_sidenav { click_on t('account.navigation.history') }
expect(page).to have_content(
t('event_types.authenticated_at', service_provider: identity_without_link.display_name),
)
@@ -51,31 +55,29 @@
expect(page).to have_link(
identity_with_link.display_name, href: 'http://localhost:3000'
)
-
- visit account_connected_accounts_path
- expect(identity_without_link_timestamp).to appear_before(identity_with_link_timestamp)
end
scenario 'revoking consent from an SP' do
identity_to_revoke = identity_with_link
- visit account_history_path
- expect(page).to have_content(
- t('event_types.authenticated_at', service_provider: identity_to_revoke.display_name),
- )
-
- visit account_connected_accounts_path
- within(find('.profile-info-box')) do
- within(find('.grid-row', text: identity_to_revoke.service_provider_record.friendly_name)) do
- click_link(t('account.revoke_consent.link_title'))
- end
+ within('.profile-info-box .grid-row', text: identity_to_revoke.display_name) do
+ click_link(t('account.revoke_consent.link_title'))
end
- expect(page).to have_content(identity_to_revoke.service_provider_record.friendly_name)
+ expect(page).to have_content(identity_to_revoke.display_name)
+
+ # Canceling should return to the Connected Accounts page
+ click_on t('links.cancel')
+ expect(page).to have_current_path(account_connected_accounts_path)
+
+ # Revoke again and confirm revocation
+ within('.profile-info-box .grid-row', text: identity_to_revoke.display_name) do
+ click_link(t('account.revoke_consent.link_title'))
+ end
click_on t('forms.buttons.continue')
# Accounts page should no longer list this app in the applications section
- expect(page).to_not have_content(identity_to_revoke.service_provider_record.friendly_name)
+ expect(page).to_not have_content(identity_to_revoke.display_name)
end
def build_account_connected_apps
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index 2e722d7f654..ac095c00bd5 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -71,7 +71,8 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_name: 'ResolutionMock',
- vendor_workflow: nil }
+ vendor_workflow: nil,
+ verified_attributes: nil }
end
let(:base_proofing_results) do
@@ -95,7 +96,8 @@
timed_out: false,
transaction_id: '',
vendor_name: 'ResidentialAddressNotRequired',
- vendor_workflow: nil },
+ vendor_workflow: nil,
+ verified_attributes: nil },
state_id: state_id_resolution,
threatmetrix: threatmetrix_response,
},
@@ -124,7 +126,8 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_name: 'ResolutionMock',
- vendor_workflow: nil },
+ vendor_workflow: nil,
+ verified_attributes: nil },
state_id: state_id_resolution,
threatmetrix: threatmetrix_response,
},
@@ -226,12 +229,12 @@
proofing_components: lexis_nexis_address_proofing_components,
},
:idv_enter_password_submitted => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'legacy_unsupervised',
proofing_components: lexis_nexis_address_proofing_components
},
'IdV: final resolution' => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'legacy_unsupervised',
profile_history: match_array(kind_of(Idv::ProfileLogging)),
proofing_components: lexis_nexis_address_proofing_components
@@ -341,12 +344,12 @@
proofing_components: lexis_nexis_address_proofing_components,
},
:idv_enter_password_submitted => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'legacy_unsupervised',
proofing_components: lexis_nexis_address_proofing_components
},
'IdV: final resolution' => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'legacy_unsupervised',
profile_history: match_array(kind_of(Idv::ProfileLogging)),
proofing_components: lexis_nexis_address_proofing_components
@@ -442,11 +445,11 @@
proofing_components: gpo_letter_proofing_components
},
:idv_enter_password_submitted => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
proofing_components: gpo_letter_proofing_components
},
'IdV: final resolution' => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
# NOTE: pending_profile_idv_level should be set here, a nil value is cached for current_user.pending_profile.
profile_history: match_array(kind_of(Idv::ProfileLogging)),
proofing_components: gpo_letter_proofing_components
@@ -558,11 +561,11 @@
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' },
},
:idv_enter_password_submitted => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, proofing_workflow_time_in_seconds: 0.0,
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }
},
'IdV: final resolution' => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, proofing_workflow_time_in_seconds: 0.0,
# NOTE: pending_profile_idv_level should be set here, a nil value is cached for current_user.pending_profile.
profile_history: match_array(kind_of(Idv::ProfileLogging)),
proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }
@@ -680,12 +683,12 @@
proofing_components: lexis_nexis_address_proofing_components,
},
:idv_enter_password_submitted => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'unsupervised_with_selfie',
proofing_components: lexis_nexis_address_proofing_components
},
'IdV: final resolution' => {
- success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false,
+ success: true, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, proofing_workflow_time_in_seconds: 0.0,
active_profile_idv_level: 'unsupervised_with_selfie',
profile_history: match_array(kind_of(Idv::ProfileLogging)),
proofing_components: lexis_nexis_address_proofing_components
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index 215c19fe083..0cd487c1a84 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -9,6 +9,7 @@
include SpAuthHelper
include IdvHelper
include DocAuthHelper
+ include AbTestsHelper
context 'service provider is on the ialmax allow list' do
before do
@@ -885,6 +886,12 @@
allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true)
allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true)
allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2)
+ allow(IdentityConfig.store).to receive(:sign_in_recaptcha_percent_tested).and_return(100)
+ reload_ab_tests
+ end
+
+ after do
+ reload_ab_tests
end
it 'redirects user to security check failed page' do
diff --git a/spec/forms/sign_in_recaptcha_form_spec.rb b/spec/forms/sign_in_recaptcha_form_spec.rb
index 65c7e0328f4..6887ab3b187 100644
--- a/spec/forms/sign_in_recaptcha_form_spec.rb
+++ b/spec/forms/sign_in_recaptcha_form_spec.rb
@@ -1,15 +1,24 @@
require 'rails_helper'
+require 'query_tracker'
RSpec.describe SignInRecaptchaForm do
let(:user) { create(:user, :with_authenticated_device) }
let(:score_threshold_config) { 0.2 }
let(:analytics) { FakeAnalytics.new }
let(:email) { user.email }
+ let(:ab_test_bucket) { :sign_in_recaptcha }
let(:recaptcha_token) { 'token' }
let(:device_cookie) { Random.hex }
let(:score) { 1.0 }
subject(:form) do
- described_class.new(form_class: RecaptchaMockForm, analytics:, score:)
+ described_class.new(
+ email:,
+ device_cookie:,
+ ab_test_bucket:,
+ form_class: RecaptchaMockForm,
+ analytics:,
+ score:,
+ )
end
before do
allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).
@@ -30,12 +39,15 @@
).
and_return(recaptcha_form)
- form.submit(email:, recaptcha_token:, device_cookie:)
+ form.submit(recaptcha_token:)
end
context 'with custom recaptcha form class' do
subject(:form) do
described_class.new(
+ email:,
+ device_cookie:,
+ ab_test_bucket:,
analytics:,
form_class: RecaptchaForm,
)
@@ -49,13 +61,48 @@
expect(RecaptchaForm).to receive(:new).and_return(recaptcha_form)
expect(recaptcha_form).to receive(:submit)
- form.submit(email:, recaptcha_token:, device_cookie:)
+ form.submit(recaptcha_token:)
+ end
+ end
+
+ describe '#exempt?' do
+ subject(:exempt?) { form.exempt? }
+
+ it { is_expected.to eq(false) }
+
+ context 'when not part of a/b test' do
+ let(:ab_test_bucket) { nil }
+
+ it { is_expected.to eq(true) }
+
+ it { expect(queries_database?).to eq(false) }
+ end
+
+ context 'score threshold configured at zero' do
+ let(:score_threshold_config) { 0.0 }
+
+ it { is_expected.to eq(true) }
+
+ it { expect(queries_database?).to eq(false) }
+ end
+
+ context 'existing device for user' do
+ let(:device_cookie) { user.devices.first.cookie_uuid }
+
+ it { is_expected.to eq(true) }
+
+ it { expect(queries_database?).to eq(true) }
+ end
+
+ def queries_database?
+ user
+ QueryTracker.track { exempt? }.present?
end
end
describe '#submit' do
let(:recaptcha_form_success) { false }
- subject(:response) { form.submit(email:, recaptcha_token:, device_cookie:) }
+ subject(:response) { form.submit(recaptcha_token:) }
context 'recaptcha form validates as unsuccessful' do
let(:score) { 0.0 }
@@ -68,6 +115,14 @@
end
end
+ context 'when not part of a/b test' do
+ let(:ab_test_bucket) { nil }
+
+ it 'is successful' do
+ expect(response.to_h).to eq(success: true)
+ end
+ end
+
context 'new device for user' do
it 'is unsuccessful with errors from recaptcha validation' do
expect(response.to_h).to eq(
diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb
index 21aa0aa9c29..64fac942c8f 100644
--- a/spec/forms/webauthn_setup_form_spec.rb
+++ b/spec/forms/webauthn_setup_form_spec.rb
@@ -7,9 +7,10 @@
let(:user_session) { { webauthn_challenge: webauthn_challenge } }
let(:device_name) { 'Chrome 119 on macOS' }
let(:domain_name) { 'localhost:3000' }
+ let(:attestation) { attestation_object }
let(:params) do
{
- attestation_object: attestation_object,
+ attestation_object: attestation,
client_data_json: setup_client_data_json,
name: 'mykey',
platform_authenticator: false,
@@ -26,42 +27,51 @@
describe '#submit' do
context 'when the input is valid' do
- it 'returns FormResponse with success: true and creates a webauthn configuration' do
- extra_attributes = {
- enabled_mfa_methods_count: 1,
- mfa_method_counts: { webauthn: 1 },
- multi_factor_auth_method: 'webauthn',
- authenticator_data_flags: {
- up: true,
- uv: false,
- be: true,
- bs: true,
- at: false,
- ed: true,
- },
- pii_like_keypaths: [[:mfa_method_counts, :phone]],
- }
+ context 'security key' do
+ it 'returns FormResponse with success: true and creates a webauthn configuration' do
+ extra_attributes = {
+ enabled_mfa_methods_count: 1,
+ mfa_method_counts: { webauthn: 1 },
+ multi_factor_auth_method: 'webauthn',
+ authenticator_data_flags: {
+ up: true,
+ uv: false,
+ be: true,
+ bs: true,
+ at: false,
+ ed: true,
+ },
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
+ }
- expect(subject.submit(params).to_h).to eq(
- success: true,
- errors: {},
- **extra_attributes,
- )
+ expect(subject.submit(params).to_h).to eq(
+ success: true,
+ errors: {},
+ **extra_attributes,
+ )
- user.reload
+ user.reload
- expect(user.webauthn_configurations.roaming_authenticators.count).to eq(1)
- expect(user.webauthn_configurations.roaming_authenticators.first.transports).to eq(['usb'])
- end
+ expect(user.webauthn_configurations.roaming_authenticators.count).to eq(1)
+ expect(user.webauthn_configurations.roaming_authenticators.first.transports).
+ to eq(['usb'])
+ end
- it 'sends a recovery information changed event' do
- expect(PushNotification::HttpPush).to receive(:deliver).
- with(PushNotification::RecoveryInformationChangedEvent.new(user: user))
+ it 'sends a recovery information changed event' do
+ expect(PushNotification::HttpPush).to receive(:deliver).
+ with(PushNotification::RecoveryInformationChangedEvent.new(user: user))
- subject.submit(params)
+ subject.submit(params)
+ end
+
+ it 'does not contains uuid' do
+ result = subject.submit(params)
+ expect(result.extra[:aaguid]).to eq nil
+ end
end
context 'with platform authenticator' do
+ let(:attestation) { platform_auth_attestation_object }
let(:params) do
super().merge(platform_authenticator: true, transports: 'internal,hybrid')
end
@@ -114,6 +124,11 @@
expect(result.to_h[:authenticator_data_flags]).to be_nil
end
end
+
+ it 'contains uuid' do
+ result = subject.submit(params)
+ expect(result.extra[:aaguid]).to eq aaguid
+ end
end
context 'with invalid transports' do
diff --git a/spec/forms/webauthn_verification_form_spec.rb b/spec/forms/webauthn_verification_form_spec.rb
index d01339b6544..1f298720bc6 100644
--- a/spec/forms/webauthn_verification_form_spec.rb
+++ b/spec/forms/webauthn_verification_form_spec.rb
@@ -10,6 +10,7 @@
let(:screen_lock_error) { nil }
let(:platform_authenticator) { false }
let(:client_data_json) { verification_client_data_json }
+ let(:webauthn_aaguid) { nil }
let!(:webauthn_configuration) do
return if !user
create(
@@ -18,6 +19,7 @@
credential_id: credential_id,
credential_public_key: credential_public_key,
platform_authenticator: platform_authenticator,
+ aaguid: webauthn_aaguid,
)
end
@@ -45,20 +47,24 @@
subject(:result) { form.submit }
context 'when the input is valid' do
- it 'returns successful result' do
- expect(result.to_h).to eq(
- success: true,
- webauthn_configuration_id: webauthn_configuration.id,
- )
+ context 'security key' do
+ it 'returns successful result' do
+ expect(result.to_h).to eq(
+ success: true,
+ webauthn_configuration_id: webauthn_configuration.id,
+ )
+ end
end
context 'for platform authenticator' do
let(:platform_authenticator) { true }
+ let(:webauthn_aaguid) { aaguid }
it 'returns successful result' do
expect(result.to_h).to eq(
success: true,
webauthn_configuration_id: webauthn_configuration.id,
+ webauthn_aaguid: aaguid,
)
end
end
diff --git a/spec/jobs/reports/fraud_metrics_report_spec.rb b/spec/jobs/reports/fraud_metrics_report_spec.rb
index 23c108d41aa..0c7cc22e981 100644
--- a/spec/jobs/reports/fraud_metrics_report_spec.rb
+++ b/spec/jobs/reports/fraud_metrics_report_spec.rb
@@ -2,6 +2,7 @@
RSpec.describe Reports::FraudMetricsReport do
let(:report_date) { Date.new(2021, 3, 2).in_time_zone('UTC').end_of_day }
+ let(:time_range) { report_date.all_month }
subject(:report) { Reports::FraudMetricsReport.new(report_date) }
let(:name) { 'fraud-metrics-report' }
@@ -13,6 +14,8 @@
let(:expected_s3_paths) do
[
"#{report_folder}/lg99_metrics.csv",
+ "#{report_folder}/suspended_metrics.csv",
+ "#{report_folder}/reinstated_metrics.csv",
]
end
@@ -26,8 +29,29 @@
let(:mock_identity_verification_lg99_data) do
[
- ['Metric', 'Total'],
- ['Unique users seeing LG-99', 5],
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users seeing LG-99', 5, time_range.begin.to_s,
+ time_range.end.to_s],
+ ]
+ end
+ let(:mock_suspended_metrics_table) do
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users suspended', 2, time_range.begin.to_s,
+ time_range.end.to_s],
+ ['Average Days Creation to Suspension', 1.5, time_range.begin.to_s,
+ time_range.end.to_s],
+ ['Average Days Proofed to Suspension', 2.0, time_range.begin.to_s,
+ time_range.end.to_s],
+ ]
+ end
+ let(:mock_reinstated_metrics_table) do
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users reinstated', 1, time_range.begin.to_s,
+ time_range.end.to_s],
+ ['Average Days to Reinstatement', 3.0, time_range.begin.to_s,
+ time_range.end.to_s],
]
end
@@ -54,6 +78,12 @@
allow(report.fraud_metrics_lg99_report).to receive(:lg99_metrics_table).
and_return(mock_identity_verification_lg99_data)
+
+ allow(report.fraud_metrics_lg99_report).to receive(:suspended_metrics_table).
+ and_return(mock_suspended_metrics_table)
+
+ allow(report.fraud_metrics_lg99_report).to receive(:reinstated_metrics_table).
+ and_return(mock_reinstated_metrics_table)
end
it 'sends out a report to just to team agnes' do
diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb
index d4c2db323c2..bb5a31a8779 100644
--- a/spec/jobs/resolution_proofing_job_spec.rb
+++ b/spec/jobs/resolution_proofing_job_spec.rb
@@ -525,6 +525,68 @@
end
end
+ context 'socure shadow mode' do
+ context 'turned on' do
+ before do
+ allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled).and_return(true)
+ end
+
+ it 'schedules a SocureShadowModeProofingJob' do
+ stub_vendor_requests
+ expect(SocureShadowModeProofingJob).to receive(:perform_later).with(
+ user_email: user.email,
+ user_uuid: user.uuid,
+ document_capture_session_result_id: document_capture_session.result_id,
+ encrypted_arguments: satisfy do |ciphertext|
+ json = JSON.parse(
+ Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(ciphertext),
+ symbolize_names: true,
+ )
+ expect(json[:applicant_pii]).to eql(
+ {
+ first_name: 'FAKEY',
+ middle_name: nil,
+ last_name: 'MCFAKERSON',
+ address1: '1 FAKE RD',
+ identity_doc_address1: '1 FAKE RD',
+ identity_doc_address2: nil,
+ identity_doc_city: 'GREAT FALLS',
+ identity_doc_address_state: 'MT',
+ identity_doc_zipcode: '59010-1234',
+ issuing_country_code: 'US',
+ address2: nil,
+ same_address_as_id: 'true',
+ city: 'GREAT FALLS',
+ state: 'MT',
+ zipcode: '59010-1234',
+ dob: '1938-10-06',
+ ssn: '900-66-1234',
+ state_id_jurisdiction: 'ND',
+ state_id_expiration: '2099-12-31',
+ state_id_issued: '2019-12-31',
+ state_id_number: '1111111111111',
+ state_id_type: 'drivers_license',
+ },
+ )
+ end,
+ service_provider_issuer: service_provider.issuer,
+ )
+
+ perform
+ end
+ end
+
+ context 'turned off' do
+ it 'does not schedule a SocureShadowModeProofingJob' do
+ stub_vendor_requests
+
+ expect(SocureShadowModeProofingJob).not_to receive(:perform_later)
+
+ perform
+ end
+ end
+ end
+
it 'determines the UUID and UUID prefix and passes it to the downstream proofing vendors' do
uuid_info = {
uuid_prefix: service_provider.app_id,
diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb
new file mode 100644
index 00000000000..3353e0e427f
--- /dev/null
+++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb
@@ -0,0 +1,381 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SocureShadowModeProofingJob do
+ let(:job) do
+ described_class.new
+ end
+
+ let(:document_capture_session) do
+ DocumentCaptureSession.create(user:).tap do |dcs|
+ dcs.create_proofing_session
+ end
+ end
+
+ let(:document_capture_session_result_id) do
+ document_capture_session.result_id
+ end
+
+ let(:applicant_pii) do
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE
+ end
+
+ let(:encrypted_arguments) do
+ Encryption::Encryptors::BackgroundProofingArgEncryptor.new.encrypt(
+ JSON.generate({ applicant_pii: }),
+ )
+ end
+
+ let(:user) { create(:user) }
+
+ let(:user_uuid) { user.uuid }
+
+ let(:user_email) { user.email }
+
+ let(:proofing_result) do
+ FormResponse.new(
+ success: true,
+ errors: {},
+ extra: {
+ exception: nil,
+ timed_out: false,
+ threatmetrix_review_status: 'pass',
+ context: {
+ device_profiling_adjudication_reason: 'device_profiling_result_pass',
+ resolution_adjudication_reason: 'pass_resolution_and_state_id',
+ should_proof_state_id: true,
+ stages: {
+ resolution: {
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: 'resolution-mock-transaction-id-123',
+ reference: 'aaa-bbb-ccc',
+ can_pass_with_additional_verification: false,
+ attributes_requiring_additional_verification: [],
+ vendor_name: 'ResolutionMock',
+ vendor_workflow: nil,
+ verified_attributes: nil,
+ },
+ residential_address: {
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: '',
+ reference: '',
+ can_pass_with_additional_verification: false,
+ attributes_requiring_additional_verification: [],
+ vendor_name: 'ResidentialAddressNotRequired',
+ vendor_workflow: nil,
+ verified_attributes: nil,
+ },
+ state_id: {
+ success: true,
+ errors: {},
+ exception: nil,
+ mva_exception: nil,
+ requested_attributes: {},
+ timed_out: false,
+ transaction_id: 'state-id-mock-transaction-id-456',
+ vendor_name: 'StateIdMock',
+ verified_attributes: [],
+ },
+ threatmetrix: {
+ client: nil,
+ success: true,
+ errors: {},
+ exception: nil,
+ timed_out: false,
+ transaction_id: 'ddp-mock-transaction-id-123',
+ review_status: 'pass',
+ response_body: {
+ "fraudpoint.score": '500',
+ request_id: '1234',
+ request_result: 'success',
+ review_status: 'pass',
+ risk_rating: 'trusted',
+ summary_risk_score: '-6',
+ tmx_risk_rating: 'neutral',
+ tmx_summary_reason_code: ['Identity_Negative_History'],
+ first_name: '[redacted]',
+ },
+ },
+ },
+ },
+ ssn_is_unique: true,
+ },
+ )
+ end
+
+ let(:socure_idplus_base_url) { 'https://example.org' }
+
+ before do
+ document_capture_session.store_proofing_result(proofing_result.to_h)
+
+ allow(IdentityConfig.store).to receive(:socure_idplus_base_url).
+ and_return(socure_idplus_base_url)
+ end
+
+ describe '#perform' do
+ subject(:perform) do
+ allow(job).to receive(:create_analytics).and_return(analytics)
+
+ job.perform(
+ document_capture_session_result_id:,
+ encrypted_arguments:,
+ service_provider_issuer: nil,
+ user_email:,
+ user_uuid:,
+ )
+ end
+
+ let(:analytics) do
+ FakeAnalytics.new
+ end
+
+ let(:socure_response_body) do
+ {
+ referenceId: 'a1234b56-e789-0123-4fga-56b7c890d123',
+ kyc: {
+ reasonCodes: [
+ 'I919',
+ 'I914',
+ 'I905',
+ ],
+ fieldValidations: {
+ firstName: 0.99,
+ surName: 0.99,
+ streetAddress: 0.99,
+ city: 0.99,
+ state: 0.99,
+ zip: 0.99,
+ mobileNumber: 0.99,
+ dob: 0.99,
+ ssn: 0.99,
+ },
+ },
+ customerProfile: {
+ customerUserId: '129',
+ userId: 'u8JpWn4QsF3R7tA2',
+ },
+ }
+ end
+
+ before do
+ stub_request(:post, 'https://example.org/api/3.0/EmailAuthScore').
+ to_return(
+ headers: {
+ 'Content-Type' => 'application/json',
+ },
+ body: JSON.generate(socure_response_body),
+ )
+ end
+
+ context 'when document capture session result is present in redis' do
+ it 'makes a proofing call' do
+ expect(job.proofer).to receive(:proof).and_call_original
+ perform
+ end
+
+ it 'does not log an idv_socure_shadow_mode_proofing_result_missing event' do
+ perform
+ expect(analytics).not_to have_logged_event(:idv_socure_shadow_mode_proofing_result_missing)
+ end
+
+ it 'logs an event' do
+ perform
+ expect(analytics).to have_logged_event(
+ :idv_socure_shadow_mode_proofing_result,
+ user_id: user.uuid,
+ resolution_result: {
+ success: true,
+ errors: {},
+ context: {
+ device_profiling_adjudication_reason: 'device_profiling_result_pass',
+ resolution_adjudication_reason: 'pass_resolution_and_state_id',
+ should_proof_state_id: true,
+ stages: {
+ residential_address: {
+ attributes_requiring_additional_verification: [],
+ can_pass_with_additional_verification: false,
+ errors: {},
+ exception: nil,
+ reference: '',
+ success: true,
+ timed_out: false,
+ transaction_id: '',
+ vendor_name: 'ResidentialAddressNotRequired',
+ vendor_workflow: nil,
+ verified_attributes: nil,
+ },
+ resolution: {
+ attributes_requiring_additional_verification: [],
+ can_pass_with_additional_verification: false,
+ errors: {},
+ exception: nil,
+ reference: 'aaa-bbb-ccc',
+ success: true,
+ timed_out: false,
+ transaction_id: 'resolution-mock-transaction-id-123',
+ vendor_name: 'ResolutionMock',
+ vendor_workflow: nil,
+ verified_attributes: nil,
+ },
+ state_id: {
+ errors: {},
+ exception: nil,
+ mva_exception: nil,
+ requested_attributes: {},
+ success: true,
+ timed_out: false,
+ transaction_id: 'state-id-mock-transaction-id-456',
+ vendor_name: 'StateIdMock',
+ verified_attributes: [],
+ },
+ threatmetrix: {
+ client: nil,
+ errors: {},
+ exception: nil,
+ review_status: 'pass',
+ success: true,
+ timed_out: false,
+ transaction_id: 'ddp-mock-transaction-id-123',
+ },
+ },
+ },
+ exception: nil,
+ ssn_is_unique: true,
+ threatmetrix_review_status: 'pass',
+ timed_out: false,
+ },
+ socure_result: {
+ attributes_requiring_additional_verification: [],
+ can_pass_with_additional_verification: false,
+ errors: { reason_codes: ['I905', 'I914', 'I919'] },
+ exception: nil,
+ reference: '',
+ success: true,
+ timed_out: false,
+ transaction_id: 'a1234b56-e789-0123-4fga-56b7c890d123',
+ vendor_name: 'socure_kyc',
+ vendor_workflow: nil,
+ verified_attributes: %i[address first_name last_name phone ssn dob].to_set,
+ },
+ )
+ end
+
+ context 'when socure proofer raises an error' do
+ before do
+ allow(job.proofer).to receive(:proof).and_raise
+ end
+
+ it 'does not squash the error' do
+ # If the Proofer encounters an error while _making_ a request, that
+ # will be returned as a Result with the `exception` property set.
+ # Other errors will be raised as normal.
+ expect { perform }.to raise_error
+ end
+ end
+ end
+
+ context 'when document capture session result is not present in redis' do
+ let(:document_capture_session_result_id) { 'some-id-that-is-not-valid' }
+
+ it 'logs an idv_socure_shadow_mode_proofing_result_missing event' do
+ perform
+
+ expect(analytics).to have_logged_event(
+ :idv_socure_shadow_mode_proofing_result_missing,
+ )
+ end
+ end
+
+ context 'when job is stale' do
+ before do
+ allow(job).to receive(:stale_job?).and_return(true)
+ end
+ it 'raises StaleJobError' do
+ expect { perform }.to raise_error(JobHelpers::StaleJobHelper::StaleJobError)
+ end
+ end
+
+ context 'when user is not found' do
+ let(:user_uuid) { 'some-user-id-that-does-not-exist' }
+ it 'raises an error' do
+ expect do
+ perform
+ end.to raise_error(RuntimeError, 'User not found: some-user-id-that-does-not-exist')
+ end
+ end
+
+ context 'when encrypted_arguments cannot be decrypted' do
+ let(:encrypted_arguments) { 'bG9sIHRoaXMgaXMgbm90IGV2ZW4gZW5jcnlwdGVk' }
+ it 'raises an error' do
+ expect { perform }.to raise_error(Encryption::EncryptionError)
+ end
+ end
+
+ context 'when encrypted_arguments contains invalid JSON' do
+ let(:encrypted_arguments) do
+ Encryption::Encryptors::BackgroundProofingArgEncryptor.new.encrypt(
+ 'this is not valid JSON',
+ )
+ end
+ it 'raises an error' do
+ expect { perform }.to raise_error(JSON::ParserError)
+ end
+ end
+ end
+
+ describe '#build_applicant' do
+ subject(:build_applicant) do
+ job.build_applicant(encrypted_arguments:, user_email:)
+ end
+
+ it 'builds an applicant structure that looks right' do
+ expect(build_applicant).to eql(
+ {
+ first_name: 'FAKEY',
+ last_name: 'MCFAKERSON',
+ address1: '1 FAKE RD',
+ address2: nil,
+ city: 'GREAT FALLS',
+ state: 'MT',
+ zipcode: '59010-1234',
+ phone: '12025551212',
+ dob: '1938-10-06',
+ ssn: '900-66-1234',
+ email: user.email,
+ },
+ )
+ end
+ end
+
+ describe '#create_analytics' do
+ it 'creates an Analytics instance with user and sp configured' do
+ analytics = job.create_analytics(
+ user:,
+ service_provider_issuer: 'some-issuer',
+ )
+ expect(analytics.sp).to eql('some-issuer')
+ expect(analytics.user).to eql(user)
+ end
+ end
+
+ describe '#proofer' do
+ it 'returns a configured proofer' do
+ allow(IdentityConfig.store).to receive(:socure_idplus_api_key).and_return('an-api-key')
+ allow(IdentityConfig.store).to receive(:socure_idplus_base_url).and_return('https://example.org')
+ allow(IdentityConfig.store).to receive(:socure_idplus_timeout_in_seconds).and_return(6)
+
+ expect(job.proofer.config.to_h).to eql(
+ api_key: 'an-api-key',
+ base_url: 'https://example.org',
+ timeout: 6,
+ )
+ end
+ end
+end
diff --git a/spec/lib/ab_test_spec.rb b/spec/lib/ab_test_spec.rb
index 7226db1b171..b6df14a9009 100644
--- a/spec/lib/ab_test_spec.rb
+++ b/spec/lib/ab_test_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe AbTest do
- subject do
+ subject(:ab_test) do
AbTest.new(
experiment_name: 'test',
buckets:,
@@ -33,7 +33,7 @@
let(:user_session) { {} }
let(:bucket) do
- subject.bucket(
+ ab_test.bucket(
request:,
service_provider:,
session:,
@@ -46,7 +46,7 @@
it 'divides random uuids into the buckets with no automatic default' do
results = {}
1000.times do
- b = subject.bucket(
+ b = ab_test.bucket(
request:,
service_provider:,
session:,
@@ -86,7 +86,7 @@
build(:user, uuid: 'some-random-uuid')
end
it 'uses uuid as discriminator' do
- expect(subject).to receive(:percent).with('some-random-uuid').once.and_call_original
+ expect(ab_test).to receive(:percent).with('some-random-uuid').once.and_call_original
expect(bucket).to eql(:foo)
end
end
@@ -141,7 +141,7 @@
let(:buckets) { { foo: 'foo', bar: 'bar' } }
it 'raises a RuntimeError' do
- expect { subject }.to raise_error(RuntimeError, 'invalid bucket data structure')
+ expect { ab_test }.to raise_error(RuntimeError, 'invalid bucket data structure')
end
end
@@ -149,7 +149,7 @@
let(:buckets) { { foo: 60, bar: 60 } }
it 'raises a RuntimeError' do
- expect { subject }.to raise_error(RuntimeError, 'bucket percentages exceed 100')
+ expect { ab_test }.to raise_error(RuntimeError, 'bucket percentages exceed 100')
end
end
@@ -157,7 +157,7 @@
let(:buckets) { [[:foo, 10], [:bar, 20]] }
it 'raises a RuntimeError' do
- expect { subject }.to raise_error(RuntimeError, 'invalid bucket data structure')
+ expect { ab_test }.to raise_error(RuntimeError, 'invalid bucket data structure')
end
end
end
@@ -165,7 +165,7 @@
describe '#include_in_analytics_event?' do
let(:event_name) { 'My cool event' }
- let(:return_value) { subject.include_in_analytics_event?(event_name) }
+ subject(:return_value) { ab_test.include_in_analytics_event?(event_name) }
context 'when should_log is nil' do
it 'returns true' do
@@ -188,6 +188,32 @@
end
end
+ context 'when object responding to `include?` is used' do
+ context 'and it matches' do
+ let(:should_log) do
+ Class.new do
+ def include?(event_name)
+ event_name == 'My cool event'
+ end
+ end.new
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'and it does not match' do
+ let(:should_log) do
+ Class.new do
+ def include?(event_name)
+ event_name == 'My not cool event'
+ end
+ end.new
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
context 'when true is used' do
let(:should_log) { true }
it 'raises' do
diff --git a/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb b/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb
index c211a9ec828..61a85cc53aa 100644
--- a/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb
+++ b/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb
@@ -5,13 +5,24 @@
let(:time_range) { Date.new(2022, 1, 1).in_time_zone('UTC').all_month }
let(:expected_lg99_metrics_table) do
[
- ['Metric', 'Total'],
- ['Unique users seeing LG-99', '5'],
- ['Unique users suspended', '2'],
- ['Average Days Creation to Suspension', '1.5'],
- ['Average Days Proofed to Suspension', '2.0'],
- ['Unique users reinstated', '1'],
- ['Average Days to Reinstatement', '3.0'],
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users seeing LG-99', '5', time_range.begin.to_s,
+ time_range.end.to_s],
+ ]
+ end
+ let(:expected_suspended_metrics_table) do
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users suspended', '2', time_range.begin.to_s, time_range.end.to_s],
+ ['Average Days Creation to Suspension', '1.5', time_range.begin.to_s, time_range.end.to_s],
+ ['Average Days Proofed to Suspension', '2.0', time_range.begin.to_s, time_range.end.to_s],
+ ]
+ end
+ let(:expected_reinstated_metrics_table) do
+ [
+ ['Metric', 'Total', 'Range Start', 'Range End'],
+ ['Unique users reinstated', '1', time_range.begin.to_s, time_range.end.to_s],
+ ['Average Days to Reinstatement', '3.0', time_range.begin.to_s, time_range.end.to_s],
]
end
let!(:user6) do
@@ -65,6 +76,28 @@
end
end
+ describe '#suspended_metrics_table' do
+ it 'renders a suspended metrics table' do
+ aggregate_failures do
+ report.suspended_metrics_table.zip(expected_suspended_metrics_table).
+ each do |actual, expected|
+ expect(actual).to eq(expected)
+ end
+ end
+ end
+ end
+
+ describe '#reinstated_metrics_table' do
+ it 'renders a reinstated metrics table' do
+ aggregate_failures do
+ report.reinstated_metrics_table.zip(expected_reinstated_metrics_table).
+ each do |actual, expected|
+ expect(actual).to eq(expected)
+ end
+ end
+ end
+ end
+
describe '#user_days_to_suspension_avg' do
context 'when there are suspended users' do
it 'returns average time to suspension' do
@@ -117,25 +150,27 @@
end
describe '#as_emailable_reports' do
- let(:expected_report) do
- Reporting::EmailableReport.new(
- title: 'LG-99 Metrics',
- filename: 'lg99_metrics',
- table: expected_lg99_metrics_table,
- )
+ let(:expected_reports) do
+ [
+ Reporting::EmailableReport.new(
+ title: 'Monthly LG-99 Metrics Jan-2022',
+ filename: 'lg99_metrics',
+ table: expected_lg99_metrics_table,
+ ),
+ Reporting::EmailableReport.new(
+ title: 'Monthly Suspended User Metrics Jan-2022',
+ filename: 'suspended_metrics',
+ table: expected_suspended_metrics_table,
+ ),
+ Reporting::EmailableReport.new(
+ title: 'Monthly Reinstated User Metrics Jan-2022',
+ filename: 'reinstated_metrics',
+ table: expected_reinstated_metrics_table,
+ ),
+ ]
end
it 'return expected table for email' do
- expect(report.as_emailable_reports).to eq expected_report
- end
- end
-
- describe '#to_csv' do
- it 'renders a csv report' do
- aggregate_failures do
- report.lg99_metrics_table.zip(expected_lg99_metrics_table).each do |actual, expected|
- expect(actual).to eq(expected)
- end
- end
+ expect(report.as_emailable_reports).to eq expected_reports
end
end
diff --git a/spec/services/encryption/contextless_kms_client_spec.rb b/spec/services/encryption/contextless_kms_client_spec.rb
index d2c1e65cc38..1cc5858418b 100644
--- a/spec/services/encryption/contextless_kms_client_spec.rb
+++ b/spec/services/encryption/contextless_kms_client_spec.rb
@@ -150,10 +150,11 @@
it 'logs the encryption' do
expect(Encryption::KmsLogger).to receive(:log).with(
:encrypt,
+ log_context: { context: 'abc' },
key_id: IdentityConfig.store.aws_kms_key_id,
)
- subject.encrypt(long_kms_plaintext)
+ subject.encrypt(long_kms_plaintext, log_context: { context: 'abc' })
end
end
@@ -185,10 +186,11 @@
it 'logs the decryption' do
expect(Encryption::KmsLogger).to receive(:log).with(
:decrypt,
+ log_context: { context: 'abc' },
key_id: IdentityConfig.store.aws_kms_key_id,
)
- subject.decrypt('KMSx' + kms_ciphertext)
+ subject.decrypt('KMSx' + kms_ciphertext, log_context: { context: 'abc' })
end
end
diff --git a/spec/services/encryption/kms_client_spec.rb b/spec/services/encryption/kms_client_spec.rb
index 9e9575c2d44..4c31b398eee 100644
--- a/spec/services/encryption/kms_client_spec.rb
+++ b/spec/services/encryption/kms_client_spec.rb
@@ -141,10 +141,10 @@
before do
contextless_client = Encryption::ContextlessKmsClient.new
allow(contextless_client).to receive(:decrypt).
- with('KMSx123abc').
+ with('KMSx123abc', log_context: encryption_context).
and_return('plaintext')
allow(contextless_client).to receive(:decrypt).
- with('123abc').
+ with('123abc', log_context: encryption_context).
and_return('plaintext')
allow(Encryption::ContextlessKmsClient).to receive(:new).and_return(contextless_client)
end
diff --git a/spec/services/encryption/kms_logger_spec.rb b/spec/services/encryption/kms_logger_spec.rb
index 9b3dd45cb1d..bf1acaf12f1 100644
--- a/spec/services/encryption/kms_logger_spec.rb
+++ b/spec/services/encryption/kms_logger_spec.rb
@@ -8,6 +8,7 @@
kms: {
action: 'encrypt',
encryption_context: { context: 'pii-encryption', user_uuid: '1234-abc' },
+ log_context: 'log_context',
key_id: 'super-duper-aws-kms-key-id',
},
log_filename: Idp::Constants::KMS_LOG_FILENAME,
@@ -18,6 +19,7 @@
described_class.log(
:encrypt,
context: { context: 'pii-encryption', user_uuid: '1234-abc' },
+ log_context: 'log_context',
key_id: 'super-duper-aws-kms-key-id',
)
end
@@ -29,6 +31,7 @@
kms: {
action: 'decrypt',
encryption_context: nil,
+ log_context: nil,
key_id: 'super-duper-aws-kms-key-id',
},
log_filename: Idp::Constants::KMS_LOG_FILENAME,
diff --git a/spec/services/proofing/mock/resolution_mock_client_spec.rb b/spec/services/proofing/mock/resolution_mock_client_spec.rb
index 5c8b2f9933f..61f23ddb71c 100644
--- a/spec/services/proofing/mock/resolution_mock_client_spec.rb
+++ b/spec/services/proofing/mock/resolution_mock_client_spec.rb
@@ -28,6 +28,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -51,6 +52,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -74,6 +76,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -97,6 +100,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -120,6 +124,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -143,6 +148,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -166,6 +172,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
@@ -191,6 +198,7 @@
can_pass_with_additional_verification: false,
attributes_requiring_additional_verification: [],
vendor_workflow: nil,
+ verified_attributes: nil,
)
end
end
diff --git a/spec/support/ab_tests_helper.rb b/spec/support/ab_tests_helper.rb
new file mode 100644
index 00000000000..8f8f4742a14
--- /dev/null
+++ b/spec/support/ab_tests_helper.rb
@@ -0,0 +1,8 @@
+module AbTestsHelper
+ def reload_ab_tests
+ AbTests.all.each do |(name, _)|
+ AbTests.send(:remove_const, name)
+ end
+ load('config/initializers/ab_tests.rb')
+ end
+end
diff --git a/spec/support/features/navigation_helper.rb b/spec/support/features/navigation_helper.rb
index f56528c777c..90c7872afcb 100644
--- a/spec/support/features/navigation_helper.rb
+++ b/spec/support/features/navigation_helper.rb
@@ -5,10 +5,14 @@ module NavigationHelper
# sidenav links.
def find_sidenav_delete_account_link
- find('.sidenav').find_link(t('account.links.delete_account'), href: account_delete_path)
+ within_sidenav { find_link(t('account.links.delete_account'), href: account_delete_path) }
end
def find_sidenav_forget_browsers_link
- find('.sidenav').find_link(t('account.navigation.forget_browsers'))
+ within_sidenav { find_link(t('account.navigation.forget_browsers')) }
+ end
+
+ def within_sidenav(&block)
+ within('.sidenav', &block)
end
end
diff --git a/spec/support/features/webauthn_helper.rb b/spec/support/features/webauthn_helper.rb
index 45278d60a9f..19735b95f2c 100644
--- a/spec/support/features/webauthn_helper.rb
+++ b/spec/support/features/webauthn_helper.rb
@@ -172,6 +172,15 @@ def attestation_object
HEREDOC
end
+ def platform_auth_attestation_object
+ <<~HEREDOC.chomp
+ o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzz
+ uoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIOa31Ugh6EPoj4z6b+ibq6rVF1CZ9ygzSNvMrFmY
+ aPLtpQECAyYgASFYIO6a1uIfDkbqg/pm7bHZG0oRGyCEuWZrCWd2v/2IqXCaIlggKQEHbAiyBZxS
+ 1HSBwwdjNCE4prYoHdzJWQILvDrIySo=
+ HEREDOC
+ end
+
def setup_client_data_json
<<~HEREDOC.chomp
eyJjaGFsbGVuZ2UiOiJncjEycndSVVVIWnFvNkZFSV9ZbEFnIiwibmV3X2tleXNfbWF5X2JlX2
@@ -198,6 +207,10 @@ def authenticator_data
'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAcQ=='
end
+ def aaguid
+ 'adce0002-35bc-c60a-648b-0b25f1f05503'
+ end
+
def signature
<<~HEREDOC.chomp
MEYCIQC7VHQpZasv8URBC/VYKWcuv4MrmV82UfsESKTGgV3r+QIhAO8iAduYC7XDHJjpKkrSKb
diff --git a/spec/support/have_logged_event_matcher.rb b/spec/support/have_logged_event_matcher.rb
index 86d09a48164..52e06b2ca2b 100644
--- a/spec/support/have_logged_event_matcher.rb
+++ b/spec/support/have_logged_event_matcher.rb
@@ -22,6 +22,15 @@ def failure_message
end
end
+ def failure_message_when_negated
+ adjective = attributes_matcher_description(expected_attributes) ? 'matching ' : ''
+ [
+ "Expected that FakeAnalytics would not have received #{adjective}event",
+ expected_event_name.inspect,
+ has_expected_count? ? count_failure_reason('it was received').strip : nil,
+ ].compact.join(' ')
+ end
+
def matches?(actual)
@actual = actual
diff --git a/yarn.lock b/yarn.lock
index 1a803270cc0..692899c6446 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3612,7 +3612,7 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
-fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
+fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -4951,9 +4951,9 @@ methods@~1.1.2:
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
- integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"