Skip to content
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
2 changes: 1 addition & 1 deletion app/controllers/concerns/idv/verify_info_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def add_proofing_costs(results)
elsif stage == :threatmetrix
# transaction_id comes from request_id
tmx_id = hash[:transaction_id]
log_irs_tmx_fraud_check_event(hash) if tmx_id
log_irs_tmx_fraud_check_event(hash, current_user) if tmx_id
add_cost(:threatmetrix, transaction_id: tmx_id) if tmx_id
end
end
Expand Down
5 changes: 5 additions & 0 deletions app/models/fraud_review_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class FraudReviewRequest < ApplicationRecord
include NonNullUuid

belongs_to :user
end
2 changes: 1 addition & 1 deletion app/models/phone_number_opt_out.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Represents a record of a phone number that has beed opted out of SMS in AWS Pinpoint
# Represents a record of a phone number that has been opted out of SMS in AWS Pinpoint
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happened across this while looking at how NonNullUuid was used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooops good catch

# AWS maintains separate opt-out lists per region, so this helps us keep track across regions
class PhoneNumberOptOut < ApplicationRecord
include NonNullUuid
Expand Down
34 changes: 34 additions & 0 deletions app/models/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def activate

def activate_after_passing_review
update!(fraud_review_pending: false, fraud_rejection: false)
track_fraud_review_adjudication(decision: 'pass')
activate
end

Expand All @@ -62,6 +63,9 @@ def deactivate_for_fraud_review

def reject_for_fraud(notify_user:)
update!(active: false, fraud_review_pending: false, fraud_rejection: true)
track_fraud_review_adjudication(
decision: notify_user ? 'manual_reject' : 'automatic_reject',
)
UserAlerts::AlertUserAboutAccountRejected.call(user) if notify_user
end

Expand Down Expand Up @@ -120,8 +124,38 @@ def has_proofed_before?
Profile.where(user_id: user_id).where.not(activated_at: nil).where.not(id: self.id).exists?
end

def irs_attempts_api_tracker
@irs_attempts_api_tracker ||= IrsAttemptsApi::Tracker.new(
session_id: nil,
request: nil,
user: user,
sp: initiating_service_provider,
cookie_device_uuid: nil,
sp_request_uri: nil,
enabled_for_session: initiating_service_provider&.irs_attempts_api_enabled?,
analytics: Analytics.new(
user: user,
request: nil,
sp: initiating_service_provider&.issuer,
session: {},
ahoy: nil,
),
)
end

private

def track_fraud_review_adjudication(decision:)
if IdentityConfig.store.irs_attempt_api_track_idv_fraud_review
fraud_review_request = user.fraud_review_requests.last
irs_attempts_api_tracker.fraud_review_adjudicated(
decision: decision,
cached_irs_session_id: fraud_review_request&.irs_session_id,
cached_login_session_id: fraud_review_request&.login_session_id,
)
end
end

def personal_key_generator
@personal_key_generator ||= PersonalKeyGenerator.new(user)
end
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class User < ApplicationRecord
source: :service_provider_record
has_many :sign_in_restrictions, dependent: :destroy
has_many :in_person_enrollments, dependent: :destroy
has_many :fraud_review_requests, dependent: :destroy

has_one :pending_in_person_enrollment,
-> { where(status: :pending).order(created_at: :desc) },
Expand Down
1 change: 1 addition & 0 deletions app/services/cloud_front_header_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def client_port

# Source IP and port for client connection to CloudFront
def viewer_address
return nil unless @request&.headers
@request.headers['CloudFront-Viewer-Address']
end
end
22 changes: 14 additions & 8 deletions app/services/idv/steps/threat_metrix_step_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,24 @@ def threatmetrix_iframe_url(session_id)
)
end

def log_irs_tmx_fraud_check_event(result)
def log_irs_tmx_fraud_check_event(result, user)
return unless IdentityConfig.store.irs_attempt_api_track_tmx_fraud_check_event
return unless FeatureManagement.proofing_device_profiling_collecting_enabled?

success = result[:review_status] == 'pass'

if !success && (tmx_summary_reason_code = result.dig(
:response_body,
:tmx_summary_reason_code,
))
failure_reason = {
tmx_summary_reason_code: tmx_summary_reason_code,
}
unless success
FraudReviewRequest.create(
user: user,
irs_session_id: irs_attempts_api_session_id,
login_session_id: Digest::SHA1.hexdigest(user.unique_session_id.to_s),
)

if (tmx_summary_reason_code = result.dig(:response_body, :tmx_summary_reason_code))
failure_reason = {
tmx_summary_reason_code: tmx_summary_reason_code,
}
end
end

irs_attempts_api_tracker.idv_tmx_fraud_check(
Expand Down
2 changes: 1 addition & 1 deletion app/services/irs_attempts_api/tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(session_id:, request:, user:, sp:, cookie_device_uuid:,
end

def track_event(event_type, metadata = {})
return if !enabled?
return unless enabled?
Comment thread
zachmargolis marked this conversation as resolved.
Outdated

if metadata.has_key?(:failure_reason) &&
(metadata[:failure_reason].blank? ||
Expand Down
13 changes: 13 additions & 0 deletions app/services/irs_attempts_api/tracker_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ def forgot_password_new_password_submitted(success:, failure_reason: nil)
)
end

# @param [String] decision One of 'pass', 'manual_reject', or 'automated_reject'
# @param [String] cached_irs_session_id The IRS session id ('tid') the user had when flagged
# @param [String] cached_login_session_id The Login.gov session id the user had when flagged
# A profile offlined for review has been approved or rejected.
def fraud_review_adjudicated(decision:, cached_irs_session_id:, cached_login_session_id:)
track_event(
:fraud_review_adjudicated,
decision: decision,
cached_irs_session_id: cached_irs_session_id,
cached_login_session_id: cached_login_session_id,
)
end

# @param ["mobile", "desktop"] upload_method method chosen for uploading id verification
# A user has selected id document upload method
def idv_document_upload_method_selected(upload_method:)
Expand Down
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ irs_attempt_api_event_ttl_seconds: 86400
irs_attempt_api_event_count_default: 1000
irs_attempt_api_event_count_max: 10000
irs_attempt_api_payload_size_logging_enabled: true
irs_attempt_api_track_idv_fraud_review: false
irs_attempt_api_track_tmx_fraud_check_event: false
key_pair_generation_percent: 0
logins_per_ip_track_only_mode: false
Expand Down
14 changes: 14 additions & 0 deletions db/primary_migrate/20230322000756_create_fraud_review_requests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CreateFraudReviewRequests < ActiveRecord::Migration[7.0]
def change
create_table :fraud_review_requests do |t|
t.integer :user_id
t.string :uuid
t.string :irs_session_id
t.string :login_session_id

t.timestamps

t.index :user_id, unique: false
end
end
end
12 changes: 11 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_03_20_235607) do
ActiveRecord::Schema[7.0].define(version: 2023_03_22_000756) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
Expand Down Expand Up @@ -226,6 +226,16 @@
t.index ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at"
end

create_table "fraud_review_requests", force: :cascade do |t|
t.integer "user_id"
t.string "uuid"
t.string "irs_session_id"
t.string "login_session_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_fraud_review_requests_on_user_id"
end

create_table "iaa_gtcs", force: :cascade do |t|
t.string "gtc_number", null: false
t.integer "mod_number", default: 0, null: false
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def self.build_store(config_map)
config.add(:irs_attempt_api_event_count_default, type: :integer)
config.add(:irs_attempt_api_event_count_max, type: :integer)
config.add(:irs_attempt_api_payload_size_logging_enabled, type: :boolean)
config.add(:irs_attempt_api_track_idv_fraud_review, type: :boolean)
config.add(:irs_attempt_api_track_tmx_fraud_check_event, type: :boolean)
config.add(:irs_attempt_api_public_key)
config.add(:irs_attempt_api_public_key_id)
Expand Down
130 changes: 130 additions & 0 deletions spec/models/profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,69 @@

expect(profile).to be_active
end

context 'when the initiating_sp is the IRS' do
let(:sp) { create(:service_provider, :irs) }
let(:profile) do
create(
:profile,
user: user,
active: false,
fraud_review_pending: true,
initiating_service_provider: sp,
)
end

context 'when the feature flag is enabled' do
before do
allow(IdentityConfig.store).to receive(:irs_attempt_api_track_idv_fraud_review).
and_return(true)
end

it 'logs an attempt event' do
allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true)
expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_truthy

expect(profile.irs_attempts_api_tracker).to receive(:fraud_review_adjudicated).
with(
hash_including(decision: 'pass'),
)
profile.activate_after_passing_review
end
end

context 'when the feature flag is disabled' do
before do
allow(IdentityConfig.store).to receive(:irs_attempt_api_track_idv_fraud_review).
and_return(false)
end

it 'does not log an attempt event' do
allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true)
expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_truthy

expect(profile.irs_attempts_api_tracker).not_to receive(:fraud_review_adjudicated)
profile.activate_after_passing_review
end
end
end

context 'when the initiating_sp is not the IRS' do
it 'does not log an attempt event' do
sp = create(:service_provider)
profile = create(
:profile,
user: user,
active: false,
fraud_review_pending: true,
initiating_service_provider: sp,
)
expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_falsey
Comment thread
n1zyy marked this conversation as resolved.
Outdated

expect(profile.irs_attempts_api_tracker).not_to receive(:fraud_review_adjudicated)
profile.activate_after_passing_review
end
end
end

describe '#deactivate_for_fraud_review' do
Expand Down Expand Up @@ -353,6 +416,73 @@
expect { profile }.to change(ActionMailer::Base.deliveries, :count).by(0)
end
end

context 'when the SP is the IRS' do
let(:sp) { create(:service_provider, :irs) }
let(:profile) do
create(
:profile,
user: user,
active: false,
fraud_review_pending: true,
initiating_service_provider: sp,
)
end

context 'and notify_user is true' do
it 'logs an event with manual_reject' do
allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true)
allow(IdentityConfig.store).to receive(:irs_attempt_api_track_idv_fraud_review).
and_return(true)

expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_truthy

expect(profile.irs_attempts_api_tracker).to receive(:fraud_review_adjudicated).
with(
hash_including(decision: 'manual_reject'),
)

profile.reject_for_fraud(notify_user: true)
end
end

context 'and notify_user is false' do
it 'logs an event with automatic_reject' do
allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true)
allow(IdentityConfig.store).to receive(:irs_attempt_api_track_idv_fraud_review).
and_return(true)

expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_truthy

expect(profile.irs_attempts_api_tracker).to receive(:fraud_review_adjudicated).
with(
hash_including(decision: 'automatic_reject'),
)

profile.reject_for_fraud(notify_user: false)
end
end
end

context 'when the SP is not the IRS' do
it 'does not log an event' do
sp = create(:service_provider)
profile = create(
:profile,
user: user,
active: false,
fraud_review_pending: true,
initiating_service_provider: sp,
)
allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true)

expect(profile.initiating_service_provider.irs_attempts_api_enabled?).to be_falsey

expect(profile.irs_attempts_api_tracker).not_to receive(:fraud_review_adjudicated)

profile.reject_for_fraud(notify_user: true)
end
end
end

describe 'scopes' do
Expand Down
18 changes: 17 additions & 1 deletion spec/services/cloud_front_header_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,23 @@

describe '#client_port' do
it 'returns nil' do
expect(subject.client_port).to eq nil
expect(subject.client_port).to be nil
end
end
end

context 'with no request included' do
let(:req) { nil }

describe '#viewer_address' do
it 'returns nil' do
expect(subject.viewer_address).to be nil
end
end

describe '#client_port' do
it 'returns nil' do
expect(subject.client_port).to be nil
end
end
end
Expand Down