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
67 changes: 33 additions & 34 deletions app/jobs/threat_metrix_js_verification_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,42 @@ class ThreatMetrixJsVerificationJob < ApplicationJob

def perform(session_id: SecureRandom.uuid)
org_id = IdentityConfig.store.lexisnexis_threatmetrix_org_id

return if org_id.blank?

return if !IdentityConfig.store.proofing_device_profiling_collecting_enabled
js = nil
valid = nil
error = nil
signature = nil

# Certificate is stored ASCII-armored in config
raw_cert = IdentityConfig.store.lexisnexis_threatmetrix_js_signing_cert
return if raw_cert.blank?

cert = OpenSSL::X509::Certificate.new raw_cert

raise 'Certificate is expired' if cert.not_after < Time.zone.now
cert = OpenSSL::X509::Certificate.new(raw_cert) if raw_cert.present?
raise 'JS signing certificate is missing' if !cert
raise 'JS signing certificate is expired' if cert.not_after < Time.zone.now

url = "https://h.online-metrix.net/fp/tags.js?org_id=#{org_id}&session_id=#{session_id}"

resp = build_faraday.get url

content, signature = parse_js resp.body

log_payload = {
name: 'ThreatMetrixJsVerification',
org_id: org_id,
session_id: session_id,
http_status: resp.status,
signature: (signature || '').each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join,
}

if verify_js content, signature, cert
log_payload[:valid] = true
else
# When signature validation fails, we include the JS payload in the
# log message for future analysis
log_payload[:valid] = false
log_payload[:js] = content
end

logger.info(log_payload.to_json)
resp = build_faraday.get(url)
content, signature = parse_js(resp.body)

valid = js_verified?(content, signature, cert)
# When signature validation fails, we include the JS payload in the
# log message for future analysis
js = content if !valid
rescue => err
error = err
raise err
ensure
logger.info(
{
name: 'ThreatMetrixJsVerification',
org_id: org_id,
session_id: session_id,
http_status: resp&.status,
signature: (signature || '').each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join,
js: js,
valid: valid,
error_class: error&.class,
error_message: error&.message,
}.compact.to_json,
)
end

def build_faraday
Expand Down Expand Up @@ -67,12 +66,12 @@ def parse_js(raw)
[content, signature]
end

def verify_js(js, signature, cert)
def js_verified?(js, signature, cert)
return false if signature.nil?

public_key = cert&.public_key
return false if public_key.nil?

public_key.verify 'SHA256', signature, js
public_key.verify('SHA256', signature, js)
end
end
37 changes: 17 additions & 20 deletions spec/jobs/threat_metrix_js_verification_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,34 +78,31 @@
)
end

context 'when collecting is disabled' do
let(:proofing_device_profiling_collecting_enabled) { false }
it 'does not run' do
expect(instance.logger).not_to receive(:info)
perform
end
end

context 'when certificate is not configured' do
let(:threatmetrix_signing_certificate) { '' }
it 'does not run' do
expect(instance.logger).not_to receive(:info)
perform
it 'logs an error_message, and raises' do
expect(instance.logger).to receive(:info) do |message|
expect(JSON.parse(message, symbolize_names: true)).to include(
name: 'ThreatMetrixJsVerification',
error_message: 'JS signing certificate is missing',
)
end

expect { perform }.to raise_error(RuntimeError, 'JS signing certificate is missing')
end
end

context 'when certificate is expired' do
let(:threatmetrix_signing_cert_expiry) { Time.zone.now - 3600 }
it 'raises an error' do
expect { perform }.to raise_error
end
end
it 'logs an error_message, and raises' do
expect(instance.logger).to receive(:info) do |message|
expect(JSON.parse(message, symbolize_names: true)).to include(
name: 'ThreatMetrixJsVerification',
error_message: 'JS signing certificate is expired',
)
end

context 'when org id is not configured' do
let(:threatmetrix_org_id) { nil }
it 'does not run' do
expect(instance.logger).not_to receive(:info)
perform
expect { perform }.to raise_error(RuntimeError, 'JS signing certificate is expired')
end
end

Expand Down