-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2696 from internetee/verify-contacts
Handling contact verifications
- Loading branch information
Showing
25 changed files
with
898 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,3 +20,4 @@ | |
/node_modules | ||
/import | ||
ettevotja_rekvisiidid__lihtandmed.csv.zip | ||
Dockerfile.dev |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
app/controllers/eeid/webhooks/identification_requests_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.