Merge pull request #2696 from internetee/verify-contacts

Handling contact verifications
This commit is contained in:
Timo Võhmar 2024-11-19 10:34:53 +02:00 committed by GitHub
commit 5d454febb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 898 additions and 7 deletions

1
.gitignore vendored
View file

@ -20,3 +20,4 @@
/node_modules /node_modules
/import /import
ettevotja_rekvisiidid__lihtandmed.csv.zip ettevotja_rekvisiidid__lihtandmed.csv.zip
Dockerfile.dev

View file

@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
def comma_support_for(parent_key, key) def comma_support_for(parent_key, key)
return if params[parent_key].blank? return if params[parent_key].blank?
return if params[parent_key][key].blank? return if params[parent_key][key].blank?
params[parent_key][key].sub!(/,/, '.') params[parent_key][key].sub!(/,/, '.')
end end

View file

@ -0,0 +1,96 @@
# frozen_string_literal: true
module Eeid
module Webhooks
# Controller for handling eeID identification requests webhook
class IdentificationRequestsController < ActionController::Base
skip_before_action :verify_authenticity_token
THROTTLED_ACTIONS = %i[create].freeze
include Shunter::Integration::Throttle
rescue_from Shunter::ThrottleError, with: :handle_throttle_error
# POST /eeid/webhooks/identification_requests
def create
return render_unauthorized unless ip_whitelisted?
return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature'])
contact = Contact.find_by_code(permitted_params[:reference])
poi = catch_poi
verify_contact(contact)
inform_registrar(contact, poi)
render json: { status: 'success' }, status: :ok
rescue StandardError => e
handle_error(e)
end
private
def permitted_params
params.permit(:identification_request_id, :reference, :client_id)
end
def render_unauthorized
render json: { error: "IPAddress #{request.remote_ip} not authorized" }, status: :unauthorized
end
def render_invalid_signature
render json: { error: 'Invalid HMAC signature' }, status: :unauthorized
end
def valid_hmac_signature?(hmac_signature)
secret = ENV['ident_service_client_secret']
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)
ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature)
end
def verify_contact(contact)
ref = permitted_params[:reference]
if contact&.ident_request_sent_at.present?
contact.update(verified_at: Time.zone.now, verification_id: permitted_params[:identification_request_id])
Rails.logger.info("Contact verified: #{ref}")
else
Rails.logger.error("Valid contact not found for reference: #{ref}")
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?
allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip)
allowed_ips.include?(request.remote_ip) || Rails.env.development?
end
# Mock throttled_user using request IP
def throttled_user
# Create a mock user-like object with the request IP
OpenStruct.new(id: request.remote_ip, class: 'WebhookRequest')
end
def handle_error(error)
Rails.logger.error("Error handling webhook: #{error.message}")
render json: { error: error.message }, status: :internal_server_error
end
def handle_throttle_error
render json: { error: Shunter.default_error_message }, status: :bad_request
end
end
end
end

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] 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].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'
@ -116,6 +116,35 @@ module Repp
render_success render_success
end end
api :POST, '/repp/v1/contacts/verify/:contact_code'
desc 'Generate and send identification request to a contact'
def verify
authorize! :verify, Epp::Contact
action = Actions::ContactVerify.new(@contact)
unless action.call
handle_non_epp_errors(@contact)
return
end
data = { contact: { code: params[:id] } }
render_success(data: data)
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
@ -217,7 +246,8 @@ module Repp
end end
def contact_params def contact_params
params.require(:contact).permit(:id, :name, :email, :phone, :legal_document, params.require(:contact).permit(:id, :name, :email, :phone, :legal_document, :verified,
:verification_link,
legal_document: %i[body type], legal_document: %i[body type],
ident: [%i[ident ident_type ident_country_code]], ident: [%i[ident ident_type ident_country_code]],
addr: [%i[country_code city street zip state]]) addr: [%i[country_code city street zip state]])

View file

@ -0,0 +1,47 @@
module Actions
class ContactVerify
attr_reader :contact
def initialize(contact)
@contact = contact
end
def call
if contact.verified_at.present?
contact.errors.add(:base, :verification_exists)
return
end
create_identification_request
return false if contact.errors.any?
commit
end
private
def create_identification_request
ident_service = Eeid::IdentificationService.new
response = ident_service.create_identification_request(request_payload)
ContactMailer.identification_requested(contact: contact, link: response['link']).deliver_now
rescue Eeid::IdentError => e
Rails.logger.error e.message
contact.errors.add(:base, :verification_error)
end
def request_payload
{
claims_required: [{
type: 'sub',
value: "#{contact.ident_country_code}#{contact.ident}"
}],
reference: contact.code
}
end
def commit
@contact.update(ident_request_sent_at: Time.zone.now)
end
end
end

View file

@ -9,6 +9,14 @@ class ContactMailer < ApplicationMailer
mail(to: contact.email, bcc: old_email, subject: subject) mail(to: contact.email, bcc: old_email, subject: subject)
end end
def identification_requested(contact:, link:)
@contact = contact
@verification_link = link
subject = default_i18n_subject(contact_code: contact.code)
mail(to: contact.email, subject: subject)
end
private private
def address_processing def address_processing

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

@ -74,6 +74,7 @@ class Ability
can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:delete, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw }
can(:renew, Epp::Contact) can(:renew, Epp::Contact)
can(:transfer, Epp::Contact) can(:transfer, Epp::Contact)
can(:verify, Epp::Contact)
can(:view_password, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw } can(:view_password, Epp::Contact) { |c, pw| c.registrar_id == @user.registrar_id || c.auth_info == pw }
end end

110
app/services/eeid/base.rb Normal file
View file

@ -0,0 +1,110 @@
# frozen_string_literal: true
module Eeid
class IdentError < StandardError; end
# Base class for handling EEID identification requests.
class Base
BASE_URL = ENV['eeid_base_url']
TOKEN_ENDPOINT = '/api/auth/v1/token'
def initialize(client_id, client_secret)
@client_id = client_id
@client_secret = client_secret
@token = nil
end
def request_endpoint(endpoint, method: :get, body: nil)
Rails.logger.debug("Requesting endpoint: #{endpoint} with method: #{method}")
authenticate unless @token
request = build_request(endpoint, method, body)
response = send_request(request)
handle_response(response)
rescue StandardError => e
handle_error(e)
end
def authenticate
Rails.logger.debug("Authenticating with client_id: #{@client_id}")
uri = URI.parse("#{BASE_URL}#{TOKEN_ENDPOINT}")
request = build_auth_request(uri)
response = send_auth_request(uri, request)
handle_auth_response(response)
end
private
def build_auth_request(uri)
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Basic #{Base64.strict_encode64("#{@client_id}:#{@client_secret}")}"
request
end
def send_auth_request(uri, request)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: ssl_enabled?) do |http|
Rails.logger.debug("Sending authentication request to #{uri}")
http.request(request)
end
end
def handle_auth_response(response)
raise IdentError, "Authentication failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
@token = JSON.parse(response.body)['access_token']
Rails.logger.debug('Authentication successful, token received')
end
def build_request(endpoint, method, body)
uri = URI.parse("#{BASE_URL}#{endpoint}")
request = create_request(uri, method)
request['Authorization'] = "Bearer #{@token}"
request.body = body.to_json if body
request.content_type = 'application/json'
request
end
def create_request(uri, method)
case method.to_sym
when :get
Net::HTTP::Get.new(uri)
when :post
Net::HTTP::Post.new(uri)
else
raise IdentError, "Unsupported HTTP method: #{method}"
end
end
def send_request(request)
uri = URI.parse(request.uri.to_s)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: ssl_enabled?) do |http|
Rails.logger.debug("Sending #{request.method} request to #{uri} with body: #{request.body}")
http.request(request)
end
end
def handle_response(response)
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)
parsed_response
end
def handle_error(exception)
raise IdentError, exception.message
end
def ssl_enabled?
!%w[test].include?(Rails.env)
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Eeid
# This class handles identification services.
class IdentificationService < Base
CLIENT_ID = ENV['ident_service_client_id']
CLIENT_SECRET = ENV['ident_service_client_secret']
def initialize
super(CLIENT_ID, CLIENT_SECRET)
end
def create_identification_request(request_params)
request_endpoint('/api/ident/v1/identification_requests', method: :post, body: request_params)
end
def get_identification_request(id)
request_endpoint("/api/ident/v1/identification_requests/#{id}")
end
def get_proof_of_identity(id)
request_endpoint("/api/ident/v1/identification_requests/#{id}/proof_of_identity")
end
end
end

View file

@ -0,0 +1,34 @@
<%
contact = RegistrantPresenter.new(registrant: @contact, view: self)
registrar = RegistrarPresenter.new(registrar: @contact.registrar, view: self)
%>
Tere <%= contact.name %>,
<br><br>
Teie registripidaja <%= registrar.name %> on palunud Teil kinnitada oma isikut, et jätkata domeeni toimingutega.
<br><br>
Palun kinnitage oma isikut, et jätkata.
<br><br>
<a href="<%= @verification_link %>" style="display: inline-block; padding: 10px 20px; background-color: #007BFF; color: white; text-decoration: none; border-radius: 5px;">Jätka eeID-ga</a>
<br><br>
Kontaktandmed:<br>
<%= render 'mailers/shared/registrant/registrant.et.html', registrant: contact, with_phone: true %>
<br><br>
Probleemide korral pöörduge oma registripidaja poole:
<%= render 'mailers/shared/registrar/registrar.et.html', registrar: registrar %>
<%= render 'mailers/shared/signatures/signature.et.html' %>
<hr>
<br><br>
Hi <%= contact.name %>,
<br><br>
Your registrar <%= registrar.name %> has requested that you verify your identity in order to proceed with domain actions.
<br><br>
Please confirm your identity to continue.
<br><br>
<a href="<%= @verification_link %>" style="display: inline-block; padding: 10px 20px; background-color: #007BFF; color: white; text-decoration: none; border-radius: 5px;">Continue with eeID</a>
<br><br>
Contact information:<br>
<%= render 'mailers/shared/registrant/registrant.en.html', registrant: contact, with_phone: true %>
<br><br>
In case of problems please turn to your registrar:
<%= render 'mailers/shared/registrar/registrar.en.html', registrar: registrar %>
<%= render 'mailers/shared/signatures/signature.en.html' %>

View file

@ -0,0 +1,31 @@
<%
contact = RegistrantPresenter.new(registrant: @contact, view: self)
registrar = RegistrarPresenter.new(registrar: @contact.registrar, view: self)
%>
Tere <%= contact.name %>,
Teie registripidaja <%= registrar.name %> on palunud teil kinnitada oma isikut, et jätkata domeeni toimingutega.
Palun kinnitage oma isikut, klõpsates alloleval lingil:
<%= @verification_link %>
Kontaktandmed:
<%= render 'mailers/shared/registrant/registrant.et.text', registrant: contact, with_phone: true %>
Palun veenduge, et muudatus on korrektne ning probleemide korral pöörduge oma registripidaja poole:
<%= render 'mailers/shared/registrar/registrar.et.text', registrar: registrar %>
<%= render 'mailers/shared/signatures/signature.et.text' %>
----------------------------------------------------------------------------------
Hi <%= contact.name %>,
Your registrar <%= registrar.name %> has requested that you verify your identity in order to proceed with domain actions.
Please confirm your identity to continue by clicking the link below:
<%= @verification_link %>
Contact information:
<%= render 'mailers/shared/registrant/registrant.en.text', registrant: contact, with_phone: true %>
In case of problems please turn to your registrar:
<%= render 'mailers/shared/registrar/registrar.en.text', registrar: registrar %>
<%= render 'mailers/shared/signatures/signature.en.text' %>

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

@ -88,6 +88,9 @@ registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with comma
# Accreditation Center API # Accreditation Center API
accr_center_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas accr_center_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas
# Webhooks
webhook_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas
# Shared key for REST-WHOIS Bounces API incl. CERT # Shared key for REST-WHOIS Bounces API incl. CERT
rwhois_bounces_api_shared_key: testkey rwhois_bounces_api_shared_key: testkey
@ -256,3 +259,7 @@ allow_accr_endspoints: 'true'
whitelist_companies: whitelist_companies:
- '12345678' - '12345678'
- '87654321' - '87654321'
eeid_base_url: 'http://eid.test'
ident_service_client_id: 123
ident_service_client_secret: 321

View file

@ -9,6 +9,9 @@ en:
models: models:
contact: contact:
attributes: attributes:
base:
verification_exists: Contact already verified
verification_error: Sending identification request failed
code: code:
blank: "Required parameter missing - code" blank: "Required parameter missing - code"
too_long_contact_code: "Contact code is too long, max 100 characters" too_long_contact_code: "Contact code is too long, max 100 characters"

View file

@ -74,6 +74,8 @@ Rails.application.routes.draw do
collection do collection 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'
get 'download_poi/:id', to: 'contacts#download_poi'
end end
end end
end end
@ -372,6 +374,12 @@ Rails.application.routes.draw do
end end
end end
namespace :eeid do
namespace :webhooks do
resources :identification_requests, only: :create
end
end
# To prevent users seeing the default welcome message "Welcome aboard" from Rails # To prevent users seeing the default welcome message "Welcome aboard" from Rails
root to: redirect('admin/sign_in') root to: redirect('admin/sign_in')
end end

View file

@ -0,0 +1,9 @@
class AddVerificationFieldsToContacts < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_column :contacts, :ident_request_sent_at, :datetime
add_column :contacts, :verified_at, :datetime
add_index :contacts, :verified_at, algorithm: :concurrently
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

@ -694,7 +694,10 @@ CREATE TABLE public.contacts (
email_history character varying, email_history character varying,
registrant_publishable boolean DEFAULT false, registrant_publishable boolean DEFAULT false,
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,
verified_at timestamp without time zone,
verification_id character varying
); );
@ -4263,6 +4266,13 @@ CREATE INDEX index_contacts_on_registrar_id ON public.contacts USING btree (regi
CREATE INDEX index_contacts_on_registrar_id_and_ident_type ON public.contacts USING btree (registrar_id, ident_type); CREATE INDEX index_contacts_on_registrar_id_and_ident_type ON public.contacts USING btree (registrar_id, ident_type);
--
-- Name: index_contacts_on_verified_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_contacts_on_verified_at ON public.contacts USING btree (verified_at);
-- --
-- Name: index_csync_records_on_domain_id; Type: INDEX; Schema: public; Owner: - -- Name: index_csync_records_on_domain_id; Type: INDEX; Schema: public; Owner: -
-- --
@ -5602,6 +5612,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230710120154'), ('20230710120154'),
('20230711083811'), ('20230711083811'),
('20240816091049'), ('20240816091049'),
('20240816092636'); ('20240816092636'),
('20240903131540'),
('20240924103554'),
('20241015071505');

View file

@ -18,7 +18,8 @@ module Serializers
json = { code: obj.code, name: obj.name, ident: ident, phone: obj.phone, json = { code: obj.code, name: obj.name, ident: ident, phone: obj.phone,
created_at: obj.created_at, auth_info: obj.auth_info, email: obj.email, created_at: obj.created_at, auth_info: obj.auth_info, email: obj.email,
statuses: statuses, disclosed_attributes: obj.disclosed_attributes, statuses: statuses, disclosed_attributes: obj.disclosed_attributes,
registrar: registrar } registrar: registrar, ident_request_sent_at: obj.ident_request_sent_at,
verified_at: obj.verified_at }
json[:address] = address if @show_address json[:address] = address if @show_address
if @domain_params if @domain_params
json[:domains] = domains json[:domains] = domains

View file

@ -0,0 +1,101 @@
require 'test_helper'
class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
setup do
@contact = contacts(:john)
@secret = 'valid_secret'
ENV['ident_service_client_secret'] = @secret
payload = {
identification_request_id: '123',
reference: @contact.code
}
@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&.clear!
end
test 'should verify contact with valid signature and parameters' do
@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 :ok
assert_equal({ 'status' => 'success' }, JSON.parse(response.body))
assert_not_nil @contact.reload.verified_at
assert_equal @contact.verification_id, '123'
assert_notify_registrar('Successful Contact Verification')
end
test 'should return unauthorized for invalid HMAC signature' do
post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => 'invalid_signature' }
assert_response :unauthorized
assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body))
assert_emails 0
end
test 'should return unauthorized for missing parameters' do
post '/eeid/webhooks/identification_requests', params: { reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature }
assert_response :unauthorized
assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body))
assert_emails 0
end
test 'should handle internal server error gracefully' do
# Simulate an error in the verify_contact method
Contact.stub :find_by_code, ->(_) { raise StandardError, 'Simulated error' } do
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' => 'Simulated error' }, JSON.parse(response.body))
assert_emails 0
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
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
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 :bad_request
assert response.body.include?(Shunter.default_error_message)
ENV['shunter_default_threshold'] = '10000'
ENV['shunter_enabled'] = 'false'
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

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

@ -0,0 +1,98 @@
require 'test_helper'
class ReppV1ContactsVerifyTest < 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: {}
)
stub_request(:post, %r{api/ident/v1/identification_requests})
.with(
body: {
claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }],
reference: @contact.code
}
).to_return(status: 200, body: { id: '123' }.to_json, headers: { 'Content-Type' => 'application/json' })
end
def test_returns_error_when_not_found
post '/repp/v1/contacts/verify/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_verifies_contact
post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
contact = Contact.find_by(code: json[:data][:contact][:code])
assert contact.present?
assert contact.ident_request_sent_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
def test_does_not_verify_already_verified_contact
@contact.update!(verified_at: Time.zone.now - 1.day)
post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 'Contact already verified', json[:message]
end
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers
post "/repp/v1/contacts/verify/#{@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
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

View file

@ -0,0 +1,104 @@
# frozen_string_literal: true
require 'test_helper'
class IdentificationServiceTest < ActiveSupport::TestCase
def setup
@service = Eeid::IdentificationService.new
end
def test_create_identification_request_success
request_params = {
claims_required: [{
type: 'sub',
value: 'EE1234567'
}],
reference: '111:111'
}
response_body = { id: '123', status: 'created' }.to_json
stub_request(:post, %r{api/auth/v1/token})
.to_return(status: 200, body: { access_token: 'mock_token' }.to_json)
stub_request(:post, %r{api/ident/v1/identification_requests})
.with(
headers: { 'Authorization' => 'Bearer mock_token' },
body: request_params.to_json
)
.to_return(status: 201, body: response_body, headers: { 'Content-Type' => 'application/json' })
result = @service.create_identification_request(request_params)
assert_equal JSON.parse(response_body), result
assert_equal 'mock_token', @service.instance_variable_get(:@token)
end
def test_create_identification_request_failure
request_params = {
claims_required: [{
type: 'sub',
value: 'EE1234567'
}],
reference: '111:111'
}
stub_request(:post, %r{api/auth/v1/token})
.to_return(status: 200, body: { access_token: 'mock_token' }.to_json)
stub_request(:post, %r{api/ident/v1/identification_requests})
.with(
headers: { 'Authorization' => 'Bearer mock_token' },
body: request_params.to_json
)
.to_return(status: 400, body: { error: 'Bad Request' }.to_json, headers: { 'Content-Type' => 'application/json' })
assert_raises(Eeid::IdentError, 'Bad Request') do
@service.create_identification_request(request_params)
end
end
def test_get_identification_request_success
id = '123'
response_body = { id: id, status: 'completed' }.to_json
stub_request(:post, %r{api/auth/v1/token})
.to_return(status: 200, body: { access_token: 'mock_token' }.to_json)
stub_request(:get, %r{api/ident/v1/identification_requests/#{id}})
.with(headers: { 'Authorization' => 'Bearer mock_token' })
.to_return(status: 200, body: response_body, headers: { 'Content-Type' => 'application/json' })
result = @service.get_identification_request(id)
assert_equal JSON.parse(response_body), result
assert_equal 'mock_token', @service.instance_variable_get(:@token)
end
def test_get_identification_request_failure
id = '123'
stub_request(:post, %r{api/auth/v1/token})
.to_return(status: 200, body: { access_token: 'mock_token' }.to_json)
stub_request(:get, %r{api/ident/v1/identification_requests/#{id}})
.with(headers: { 'Authorization' => 'Bearer mock_token' })
.to_return(status: 404, body: { error: 'Not Found' }.to_json)
assert_raises(Eeid::IdentError, 'Not Found') do
@service.get_identification_request(id)
end
end
def test_authentication_needed_for_requests
stub_request(:post, %r{api/auth/v1/token})
.to_return(status: 401, body: { error: 'Invalid credentials' }.to_json)
assert_raises(Eeid::IdentError) do
@service.create_identification_request({ key: 'value' })
end
assert_raises(Eeid::IdentError) do
@service.get_identification_request('123')
end
assert_equal nil, @service.instance_variable_get(:@token)
end
end