Modified identification request webhook

This commit is contained in:
tsoganov 2024-10-15 14:13:04 +03:00
parent d04622c49f
commit 0085f99e02
14 changed files with 292 additions and 19 deletions

View file

@ -16,7 +16,10 @@ module Eeid
return render_unauthorized unless ip_whitelisted? return render_unauthorized unless ip_whitelisted?
return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature']) return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature'])
verify_contact(permitted_params[:reference]) contact = Contact.find_by_code(permitted_params[:reference])
poi = catch_poi
verify_contact(contact)
inform_registrar(contact, poi)
render json: { status: 'success' }, status: :ok render json: { status: 'success' }, status: :ok
rescue StandardError => e rescue StandardError => e
handle_error(e) handle_error(e)
@ -42,17 +45,32 @@ module Eeid
ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature) ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature)
end end
def verify_contact(ref) def verify_contact(contact)
contact = Contact.find_by_code(ref) ref = permitted_params[:reference]
if contact&.ident_request_sent_at.present? if contact&.ident_request_sent_at.present?
contact.update(verified_at: Time.zone.now) contact.update(verified_at: Time.zone.now, verification_id: permitted_params[:identification_request_id])
Rails.logger.info("Contact verified: #{ref}") Rails.logger.info("Contact verified: #{ref}")
else else
Rails.logger.error("Valid contact not found for reference: #{ref}") Rails.logger.error("Valid contact not found for reference: #{ref}")
end end
end end
def catch_poi
ident_service = Eeid::IdentificationService.new
response = ident_service.get_proof_of_identity(permitted_params[:identification_request_id])
raise StandardError, response[:error] if response[:error].present?
response[:data]
end
def inform_registrar(contact, poi)
email = contact&.registrar&.email
return unless email
RegistrarMailer.contact_verified(email: email, contact: contact, poi: poi)
.deliver_now
end
def ip_whitelisted? def ip_whitelisted?
allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip) allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip)
@ -67,7 +85,7 @@ module Eeid
def handle_error(error) def handle_error(error)
Rails.logger.error("Error handling webhook: #{error.message}") Rails.logger.error("Error handling webhook: #{error.message}")
render json: { error: 'Internal Server Error' }, status: :internal_server_error render json: { error: error.message }, status: :internal_server_error
end end
def handle_throttle_error def handle_throttle_error

View file

@ -2,10 +2,10 @@ require 'serializers/repp/contact'
module Repp module Repp
module V1 module V1
class ContactsController < BaseController # rubocop:disable Metrics/ClassLength class ContactsController < BaseController # rubocop:disable Metrics/ClassLength
before_action :find_contact, only: %i[show update destroy verify] before_action :find_contact, only: %i[show update destroy verify download_poi]
skip_around_action :log_request, only: :search skip_around_action :log_request, only: %i[search]
THROTTLED_ACTIONS = %i[index check search create show update destroy verify].freeze THROTTLED_ACTIONS = %i[index check search create show update destroy verify download_poi].freeze
include Shunter::Integration::Throttle include Shunter::Integration::Throttle
api :get, '/repp/v1/contacts' api :get, '/repp/v1/contacts'
@ -132,6 +132,19 @@ module Repp
render_success(data: data) render_success(data: data)
end end
api :get, '/repp/v1/contacts/download_poi/:contact_code'
desc 'Get proof of identity pdf file for a contact'
def download_poi
authorize! :verify, Epp::Contact
ident_service = Eeid::IdentificationService.new
response = ident_service.get_proof_of_identity(@contact.verification_id)
send_data response[:data], filename: "proof_of_identity_#{@contact.verification_id}.pdf",
type: 'application/pdf', disposition: 'inline'
rescue Eeid::IdentError => e
handle_non_epp_errors(@contact, e.message)
end
private private
def index_params def index_params

View file

@ -23,8 +23,8 @@ module Actions
def create_identification_request def create_identification_request
ident_service = Eeid::IdentificationService.new ident_service = Eeid::IdentificationService.new
request = ident_service.create_identification_request(request_payload) response = ident_service.create_identification_request(request_payload)
ContactMailer.identification_requested(contact: contact, link: request['link']).deliver_now ContactMailer.identification_requested(contact: contact, link: response['link']).deliver_now
rescue Eeid::IdentError => e rescue Eeid::IdentError => e
Rails.logger.error e.message Rails.logger.error e.message
contact.errors.add(:base, :verification_error) contact.errors.add(:base, :verification_error)

View file

@ -0,0 +1,10 @@
class RegistrarMailer < ApplicationMailer
helper ApplicationHelper
def contact_verified(email:, contact:, poi:)
@contact = contact
subject = 'Successful Contact Verification'
attachments['proof_of_identity.pdf'] = poi
mail(to: email, subject: subject)
end
end

View file

@ -85,10 +85,17 @@ module Eeid
end end
def handle_response(response) def handle_response(response)
parsed_response = JSON.parse(response.body) case response['content-type']
when 'application/pdf', 'application/octet-stream'
parsed_response = { data: response.body, message: response['content-disposition'] }
when %r{application/json}
parsed_response = JSON.parse(response.body).with_indifferent_access
else
raise IdentError, 'Unsupported content type'
end
raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess) raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess)
Rails.logger.debug("Request successful: #{response.body}")
parsed_response parsed_response
end end

View file

@ -17,5 +17,9 @@ module Eeid
def get_identification_request(id) def get_identification_request(id)
request_endpoint("/api/ident/v1/identification_requests/#{id}") request_endpoint("/api/ident/v1/identification_requests/#{id}")
end end
def get_proof_of_identity(id)
request_endpoint("/api/ident/v1/identification_requests/#{id}/proof_of_identity")
end
end end
end end

View file

@ -0,0 +1,44 @@
Tere,
<br><br>
<p>
Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud.
</p>
<h3>Kontaktandmed:</h3>
<ul>
<li><strong>Kood:</strong> <%= @contact.code %></li>
<li><strong>Nimi:</strong> <%= @contact.name %></li>
<li><strong>Identifikaator:</strong> <%= ident_for(@contact) %></li>
</ul>
<p>
Täielikud tulemused leiate manuses olevast PDF-failist.
</p>
Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust.
</p>
<p>
Parimate soovidega,
</p>
<%= render 'mailers/shared/signatures/signature.et.html' %>
<hr>
<br><br>
Hi,
<br><br>
<p>
We are writing to inform you that the verification process for the following contact has been successfully completed.
</p>
<h3>Contact Details:</h3>
<ul>
<li><strong>Code:</strong> <%= @contact.code %></li>
<li><strong>Name:</strong> <%= @contact.name %></li>
<li><strong>Ident:</strong> <%= ident_for(@contact) %></li>
</ul>
<p>
The full result can be found in the attached PDF file.
</p>
<p>
If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us.
</p>
<p>
Best regards,
</p>
<%= render 'mailers/shared/signatures/signature.en.html' %>

View file

@ -0,0 +1,32 @@
Tere,
Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud.
Kontaktandmed:
- Kood: <%= @contact.code %>
- Nimi: <%= @contact.name %>
- Identifikaator: <%= ident_for(@contact) %>
Täielikke tulemusi saab vaadata kontakti lehelt registripidaja portaalis.
Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust.
Parimate soovidega,
<%= render 'mailers/shared/signatures/signature.et.text' %>
---
Hi,
We are writing to inform you that the verification process for the following contact has been successfully completed.
Contact Details:
- Code: <%= @contact.code %>
- Name: <%= @contact.name %>
- Ident: <%= ident_for(@contact) %>
The full result can be viewed on the contact page in the Registrar Portal.
If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us.
Best regards,
<%= render 'mailers/shared/signatures/signature.en.text' %>

View file

@ -75,6 +75,7 @@ Rails.application.routes.draw do
get 'check/:id', to: 'contacts#check' get 'check/:id', to: 'contacts#check'
get 'search(/:id)', to: 'contacts#search' get 'search(/:id)', to: 'contacts#search'
post 'verify/:id', to: 'contacts#verify' post 'verify/:id', to: 'contacts#verify'
get 'download_poi/:id', to: 'contacts#download_poi'
end end
end end
end end

View file

@ -0,0 +1,5 @@
class AddVerificationIdToContacts < ActiveRecord::Migration[6.1]
def change
add_column :contacts, :verification_id, :string
end
end

View file

@ -696,7 +696,8 @@ CREATE TABLE public.contacts (
checked_company_at timestamp without time zone, checked_company_at timestamp without time zone,
company_register_status character varying company_register_status character varying
ident_request_sent_at timestamp without time zone, ident_request_sent_at timestamp without time zone,
verified_at timestamp without time zone verified_at timestamp without time zone,
verification_id character varying
); );
@ -5613,6 +5614,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240816091049'), ('20240816091049'),
('20240816092636'), ('20240816092636'),
('20240903131540'), ('20240903131540'),
('20240924103554'); ('20240924103554'),
('20241015071505');

View file

@ -11,6 +11,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
} }
@valid_hmac_signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json) @valid_hmac_signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
stub_request(:post, %r{api/auth/v1/token})
.to_return(
status: 200,
body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}
)
pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf'))
stub_request(:get, %r{api/ident/v1/identification_requests})
.to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' })
adapter = ENV['shunter_default_adapter'].constantize.new adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear! adapter&.clear!
end end
@ -22,6 +31,8 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
assert_response :ok assert_response :ok
assert_equal({ 'status' => 'success' }, JSON.parse(response.body)) assert_equal({ 'status' => 'success' }, JSON.parse(response.body))
assert_not_nil @contact.reload.verified_at assert_not_nil @contact.reload.verified_at
assert_equal @contact.verification_id, '123'
assert_notify_registrar('Successful Contact Verification')
end end
test 'should return unauthorized for invalid HMAC signature' do test 'should return unauthorized for invalid HMAC signature' do
@ -29,6 +40,7 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
assert_response :unauthorized assert_response :unauthorized
assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body))
assert_emails 0
end end
test 'should return unauthorized for missing parameters' do test 'should return unauthorized for missing parameters' do
@ -36,6 +48,7 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
assert_response :unauthorized assert_response :unauthorized
assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body))
assert_emails 0
end end
test 'should handle internal server error gracefully' do test 'should handle internal server error gracefully' do
@ -44,10 +57,24 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature }
assert_response :internal_server_error assert_response :internal_server_error
assert_equal({ 'error' => 'Internal Server Error' }, JSON.parse(response.body)) assert_equal({ 'error' => 'Simulated error' }, JSON.parse(response.body))
assert_emails 0
end end
end end
test 'should handle error from ident response' do
stub_request(:get, %r{api/ident/v1/identification_requests})
.to_return(status: :not_found, body: { error: 'Proof of identity not found' }.to_json, headers: { 'Content-Type' => 'application/json' })
@contact.update!(ident_request_sent_at: Time.zone.now - 1.day)
post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature }
assert_response :internal_server_error
assert_equal({ 'error' => 'Proof of identity not found' }, JSON.parse(response.body))
assert_emails 0
assert_nil @contact.reload.verified_at
end
test 'returns error response if throttled' do test 'returns error response if throttled' do
ENV['shunter_default_threshold'] = '1' ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true' ENV['shunter_enabled'] = 'true'
@ -60,4 +87,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
ENV['shunter_default_threshold'] = '10000' ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false' ENV['shunter_enabled'] = 'false'
end end
private
def assert_notify_registrar(subject)
assert_emails 1
email = ActionMailer::Base.deliveries.last
assert_equal [@contact.registrar.email], email.to
assert_equal subject, email.subject
assert_equal 1, email.attachments.size
assert_equal 'proof_of_identity.pdf', email.attachments.first.filename
end
end end

View file

@ -0,0 +1,72 @@
require 'test_helper'
class ReppV1ContactsDownloadPoiTest < ActionDispatch::IntegrationTest
def setup
@contact = contacts(:john)
@user = users(:api_bestnames)
token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
token = "Basic #{token}"
@auth_headers = { 'Authorization' => token }
adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear!
stub_request(:post, %r{api/auth/v1/token})
.to_return(
status: 200,
body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}
)
pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf'))
stub_request(:get, %r{api/ident/v1/identification_requests})
.to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' })
end
def test_returns_error_when_not_found
get '/repp/v1/contacts/download_poi/nonexistant:code', headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :not_found
assert_equal 2303, json[:code]
assert_equal 'Object does not exist', json[:message]
end
def test_downloads_poi_for_contact
@contact.update!(verified_at: Time.zone.now - 1.day, verification_id: '123')
get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers
assert_response :ok
assert_equal 'application/pdf', response.headers['Content-Type']
assert_equal "inline; filename=\"proof_of_identity_123.pdf\"; filename*=UTF-8''proof_of_identity_123.pdf", response.headers['Content-Disposition']
assert_not_empty response.body
end
def test_handles_non_epp_error
stub_request(:get, %r{api/ident/v1/identification_requests})
.to_return(
status: :not_found,
body: { error: 'Proof of identity not found' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 'Proof of identity not found', json[:message]
end
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers
get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal json[:code], 2502
assert response.body.include?(Shunter.default_error_message)
ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false'
end
end

View file

@ -12,14 +12,18 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest
adapter = ENV['shunter_default_adapter'].constantize.new adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear! adapter&.clear!
stub_request(:post, %r{api/auth/v1/token}).to_return(status: 200, body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}) stub_request(:post, %r{api/auth/v1/token})
.to_return(
status: 200,
body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}
)
stub_request(:post, %r{api/ident/v1/identification_requests}) stub_request(:post, %r{api/ident/v1/identification_requests})
.with( .with(
body: { body: {
claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }], claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }],
reference: @contact.code reference: @contact.code
} }
).to_return(status: 200, body: { id: '123' }.to_json, headers: {}) ).to_return(status: 200, body: { id: '123' }.to_json, headers: { 'Content-Type' => 'application/json' })
end end
def test_returns_error_when_not_found def test_returns_error_when_not_found
@ -43,6 +47,20 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest
assert contact.present? assert contact.present?
assert contact.ident_request_sent_at assert contact.ident_request_sent_at
assert_nil contact.verified_at assert_nil contact.verified_at
assert_notify_contact('Identification requested')
end
def test_handles_non_epp_error
stub_request(:post, %r{api/ident/v1/identification_requests})
.to_return(status: :unprocessable_entity, body: { error: 'error' }.to_json, headers: { 'Content-Type' => 'application/json' })
post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 'Sending identification request failed', json[:message]
assert_nil @contact.ident_request_sent_at
assert_emails 0
end end
def test_does_not_verify_already_verified_contact def test_does_not_verify_already_verified_contact
@ -68,4 +86,13 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest
ENV['shunter_default_threshold'] = '10000' ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false' ENV['shunter_enabled'] = 'false'
end end
private
def assert_notify_contact(subject)
assert_emails 1
email = ActionMailer::Base.deliveries.last
assert_equal [@contact.email], email.to
assert_equal subject, email.subject
end
end end