mirror of
https://github.com/internetee/registry.git
synced 2025-07-28 13:36:15 +02:00
Handling contact verifications
This commit is contained in:
parent
44e42dd0fb
commit
637cabf95f
19 changed files with 612 additions and 5 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Eeid
|
||||||
|
module Webhooks
|
||||||
|
# Controller for handling eeID identification requests webhook
|
||||||
|
class IdentificationRequestsController < ActionController::Base
|
||||||
|
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'])
|
||||||
|
|
||||||
|
verify_contact(permitted_params[:reference])
|
||||||
|
render json: { status: 'success' }, status: :ok
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("Error handling webhook: #{e.message}")
|
||||||
|
render json: { error: 'Internal Server Error' }, status: :internal_server_error
|
||||||
|
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(ref)
|
||||||
|
contact = Contact.find_by_code(ref)
|
||||||
|
|
||||||
|
if contact&.ident_request_sent_at.present?
|
||||||
|
contact.update(verified_at: Time.zone.now)
|
||||||
|
Rails.logger.info("Contact verified: #{ref}")
|
||||||
|
else
|
||||||
|
Rails.logger.error("Valid contact not found for reference: #{ref}")
|
||||||
|
end
|
||||||
|
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_throttle_error
|
||||||
|
render json: { error: Shunter.default_error_message }, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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]
|
||||||
skip_around_action :log_request, only: :search
|
skip_around_action :log_request, only: :search
|
||||||
|
|
||||||
THROTTLED_ACTIONS = %i[index check search create show update destroy].freeze
|
THROTTLED_ACTIONS = %i[index check search create show update destroy verify].freeze
|
||||||
include Shunter::Integration::Throttle
|
include Shunter::Integration::Throttle
|
||||||
|
|
||||||
api :get, '/repp/v1/contacts'
|
api :get, '/repp/v1/contacts'
|
||||||
|
@ -116,6 +116,22 @@ 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
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def index_params
|
def index_params
|
||||||
|
@ -217,7 +233,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]])
|
||||||
|
|
47
app/interactions/actions/contact_verify.rb
Normal file
47
app/interactions/actions/contact_verify.rb
Normal 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
|
||||||
|
request = ident_service.create_identification_request(request_payload)
|
||||||
|
ContactMailer.identification_requested(contact: contact, link: request['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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
103
app/services/eeid/base.rb
Normal file
103
app/services/eeid/base.rb
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# 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)
|
||||||
|
parsed_response = JSON.parse(response.body)
|
||||||
|
raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess)
|
||||||
|
|
||||||
|
Rails.logger.debug("Request successful: #{response.body}")
|
||||||
|
parsed_response
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_error(exception)
|
||||||
|
raise IdentError, exception.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssl_enabled?
|
||||||
|
!%w[test].include?(Rails.env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/services/eeid/identification_service.rb
Normal file
21
app/services/eeid/identification_service.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
end
|
|
@ -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' %>
|
|
@ -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' %>
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -74,6 +74,7 @@ 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'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -372,6 +373,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
|
||||||
|
|
|
@ -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
|
|
@ -695,6 +695,8 @@ CREATE TABLE public.contacts (
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -4263,6 +4265,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 +5611,8 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||||
('20230710120154'),
|
('20230710120154'),
|
||||||
('20230711083811'),
|
('20230711083811'),
|
||||||
('20240816091049'),
|
('20240816091049'),
|
||||||
('20240816092636');
|
('20240816092636'),
|
||||||
|
('20240903131540'),
|
||||||
|
('20240924103554');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
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' => 'Internal Server Error' }, JSON.parse(response.body))
|
||||||
|
end
|
||||||
|
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
|
||||||
|
end
|
71
test/integration/repp/v1/contacts/verify_test.rb
Normal file
71
test/integration/repp/v1/contacts/verify_test.rb
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
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: {})
|
||||||
|
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
|
||||||
|
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
|
||||||
|
end
|
104
test/services/identification_service_test.rb
Normal file
104
test/services/identification_service_test.rb
Normal 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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
Loading…
Add table
Add a link
Reference in a new issue