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
17 changes: 17 additions & 0 deletions app/controllers/socure_webhook_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SocureWebhookController < ApplicationController
def create
begin
log_webhook_receipt
repeat_webhook
process_webhook_event
rescue StandardError => e
NewRelic::Agent.notice_error(e)
Expand Down Expand Up @@ -134,4 +135,20 @@ def user
def docv_transaction_token
@docv_transaction_token ||= event[:docvTransactionToken] || event[:docVTransactionToken]
end

def repeat_webhook
endpoints = IdentityConfig.store.socure_docv_webhook_repeat_endpoints
return if endpoints.blank?

headers = {
Authorization: request.headers['Authorization'],
'Content-Type': request.headers['Content-Type'],
}

body = socure_params.to_h

endpoints.each do |endpoint|
SocureDocvRepeatWebhookJob.perform_later(body:, headers:, endpoint:)
end
end
end
10 changes: 10 additions & 0 deletions app/jobs/socure_docv_repeat_webhook_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class SocureDocvRepeatWebhookJob < ApplicationJob
queue_as :high_socure_docv

def perform(body:, headers:, endpoint:)
wr = DocAuth::Socure::WebhookRepeater.new(body:, headers:, endpoint:)
wr.repeat
end
end
70 changes: 70 additions & 0 deletions app/services/doc_auth/socure/webhook_repeater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module DocAuth
module Socure
class WebhookRepeater
attr_reader :body, :headers, :endpoint

def initialize(body:, headers:, endpoint:)
@body = body
@headers = headers
@endpoint = endpoint
end

def repeat
send_http_post_request(endpoint)
rescue => exception
NewRelic::Agent.notice_error(
exception,
custom_params: {
event: 'Failed to repeat webhook',
endpoint:,
body:,
},
)
end

private

def send_http_post_request(endpoint)
faraday_connection(endpoint).post do |req|
req.options.context = { service_name: 'socure_webhook_repeater' }
req.body = body.to_json
end
end

def faraday_connection(endpoint)
retry_options = {
max: 2,
interval: 0.05,
interval_randomness: 0.5,
backoff_factor: 2,
retry_statuses: [404, 500],
retry_block: lambda do |env:, options:, retry_count:, exception:, will_retry_in:|
NewRelic::Agent.notice_error(exception, custom_params: { retry: retry_count })
end,
}

timeout = 15

Faraday.new(url: url(endpoint).to_s, headers: request_headers) do |conn|
conn.request :retry, retry_options
conn.request :instrumentation, name: 'request_metric.faraday'
conn.adapter :net_http
conn.options.timeout = timeout
conn.options.read_timeout = timeout
conn.options.open_timeout = timeout
conn.options.write_timeout = timeout
end
end

def url(endpoint)
URI.join(endpoint)
end

def request_headers(extras = {})
headers.merge(extras)
end
end
end
end
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ socure_docv_document_request_endpoint: ''
socure_docv_enabled: false
socure_docv_verification_data_test_mode: false
socure_docv_verification_data_test_mode_tokens: '[]'
socure_docv_webhook_repeat_endpoints: '[]'
socure_docv_webhook_secret_key: ''
socure_docv_webhook_secret_key_queue: '[]'
socure_idplus_api_key: ''
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def self.store
config.add(:socure_docv_enabled, type: :boolean)
config.add(:socure_docv_verification_data_test_mode, type: :boolean)
config.add(:socure_docv_verification_data_test_mode_tokens, type: :json)
config.add(:socure_docv_webhook_repeat_endpoints, type: :json)
config.add(:socure_docv_webhook_secret_key_queue, type: :json)
config.add(:socure_docv_webhook_secret_key, type: :string)
config.add(:socure_idplus_api_key, type: :string)
Expand Down
53 changes: 53 additions & 0 deletions spec/controllers/socure_webhook_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,51 @@
before do
request.headers['Authorization'] = socure_secret_key
end

shared_examples 'repeats webhooks' do
let(:headers) do
{
Authorization: socure_secret_key_queue.last,
'Content-Type': 'application/json',
}
end
let(:endpoints) do
3.times.map { |i| "https://#{i}.example.test/endpoint" }
end
let(:repeated_body) do
b = {}.merge(webhook_body)
b[:event].delete(:data)
b
end

before do
allow(IdentityConfig.store)
.to receive(:socure_docv_webhook_repeat_endpoints).and_return(endpoints)
allow(SocureDocvResultsJob).to receive(:perform_now)
allow(SocureDocvRepeatWebhookJob).to receive(:perform_later)

dcs = create(:document_capture_session, :socure)
webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token

request.headers['Authorization'] = headers[:Authorization]
request.headers['Content-Type'] = headers[:'Content-Type']
end

context 'when idv workers are enabled' do
it 'queues SocureDocvRepeatWebhook jobs' do
endpoints.each do |endpoint|
expect(SocureDocvRepeatWebhookJob).to receive(:perform_later) do |*args|
expect(args.first[:body]).to eq(JSON.parse(repeated_body.to_json))
expect(args.first[:endpoint]).to eq(endpoint)
expect(args.first[:headers]).to eq(headers)
end
end

post :create, params: webhook_body
end
end
end

it 'returns OK and logs an event with a correct secret key and body' do
post :create, params: webhook_body

Expand Down Expand Up @@ -92,6 +137,8 @@
end
end

it_behaves_like 'repeats webhooks'

context 'when document capture session exists' do
it 'logs the user\'s uuid' do
dcs = create(:document_capture_session, :socure)
Expand All @@ -113,6 +160,8 @@
context 'when DOCUMENTS_UPLOADED event received' do
let(:event_type) { 'DOCUMENTS_UPLOADED' }

it_behaves_like 'repeats webhooks'

it 'returns OK and logs an event with a correct secret key and body' do
dcs = create(:document_capture_session, :socure)
webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token
Expand Down Expand Up @@ -181,6 +230,8 @@
context 'when SESSION_COMPLETE event received' do
let(:event_type) { 'SESSION_COMPLETE' }

it_behaves_like 'repeats webhooks'

it 'does not increment rate limiter of user' do
dcs = create(:document_capture_session, :socure)
webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token
Expand Down Expand Up @@ -220,6 +271,8 @@
context 'when SESSION_EXPIRED event received' do
let(:event_type) { 'SESSION_EXPIRED' }

it_behaves_like 'repeats webhooks'

it 'does not increment rate limiter of user' do
dcs = create(:document_capture_session, :socure)
webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token
Expand Down
26 changes: 25 additions & 1 deletion spec/features/idv/doc_auth/socure_document_capture_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' }
let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' }
let(:socure_docv_verification_data_test_mode) { false }
let(:socure_docv_webhook_repeat_endpoints) { [] }

before(:each) do
allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true)
Expand All @@ -22,6 +23,9 @@
.and_return(socure_docv_webhook_secret_key)
allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint)
.and_return(fake_socure_docv_document_request_endpoint)
allow(IdentityConfig.store).to receive(:socure_docv_webhook_repeat_endpoints)
.and_return(socure_docv_webhook_repeat_endpoints)
socure_docv_webhook_repeat_endpoints.each { |endpoint| stub_request(:post, endpoint) }
allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false)
allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
@docv_transaction_token = stub_docv_document_request
Expand All @@ -44,13 +48,23 @@
end

context 'rate limits calls to backend docauth vendor', allow_browser_log: true do
let(:socure_docv_webhook_repeat_endpoints) do # repeat webhooks
['https://1.example.test/thepath', 'https://2.example.test/thepath']
end

before do
expect(SocureDocvRepeatWebhookJob).to receive(:perform_later)
.exactly(6 * max_attempts * socure_docv_webhook_repeat_endpoints.length)
.times.and_call_original
(max_attempts - 1).times do
socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token)
end
end

it 'redirects to the rate limited error page' do
# recovers when fails to repeat webhook to an endpoint
allow_any_instance_of(DocAuth::Socure::WebhookRepeater)
.to receive(:send_http_post_request).and_raise('doh')
expect(page).to have_current_path(fake_socure_document_capture_app_url)
visit idv_socure_document_capture_path
expect(page).to have_current_path(idv_socure_document_capture_path)
Expand Down Expand Up @@ -124,6 +138,9 @@

context 'reuses valid capture app urls when appropriate', allow_browser_log: true do
context 'successfully erases capture app url when flow is complete' do
before do
expect(DocAuth::Socure::WebhookRepeater).not_to receive(:new)
end
it 'proceeds to the next page with valid info' do
document_capture_session = DocumentCaptureSession.find_by(user_id: @user.id)
expect(document_capture_session.socure_docv_capture_app_url)
Expand Down Expand Up @@ -230,7 +247,7 @@
expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil
end

xit 'does track state if state tracking is disabled' do
it 'does track state if state tracking is enabled' do
allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true)
socure_docv_upload_documents(
docv_transaction_token: @docv_transaction_token,
Expand Down Expand Up @@ -294,7 +311,14 @@
end

context 'standard mobile flow' do
let(:socure_docv_webhook_repeat_endpoints) do # repeat webhooks
['https://1.example.test/thepath', 'https://2.example.test/thepath']
end

it 'proceeds to the next page with valid info' do
expect(SocureDocvRepeatWebhookJob).to receive(:perform_later)
.exactly(6 * socure_docv_webhook_repeat_endpoints.length).times.and_call_original

perform_in_browser(:mobile) do
visit_idp_from_oidc_sp_with_ial2
@user = sign_in_and_2fa_user
Expand Down
Loading