require 'rack'
require 'rack/handler/puma'
require 'json'
require 'logger'
require 'openssl'
require 'base64'
# In the portal, you can retrieve the secret for your endpoint once it's created
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
class VerifyWebhook
def initialize(secret)
@secret = secret
end
def call(env)
request = Rack::Request.new(env)
body = request.body.read
if valid_signature?(request, body)
# Process the webhook as you'd like
return [200, {'content-type' => 'application/json'}, [{ message: "Webhook Received" }.to_json]]
else
# Handle and return the error accordingly
return [403, {'content-type' => 'text/plain'}, ['Forbidden']]
end
end
private
def valid_signature?(request, body)
webhook_id = request.env['HTTP_SVIX_ID']
webhook_timestamp = request.env['HTTP_SVIX_TIMESTAMP']
received_signatures = request.env['HTTP_SVIX_SIGNATURE'].split(' ')
signed_content = "#{webhook_id}.#{webhook_timestamp}.#{body}"
expected_signature = compute_signature(signed_content)
received_signatures.any? do |sig|
version, signature = sig.split(',')
secure_compare(signature, expected_signature)
end
end
def compute_signature(data)
# Webhooks are signed using an HMAC with SHA-256
key = Base64.decode64(@secret.split('_')[1])
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, key, data)
Base64.strict_encode64(hmac)
end
def secure_compare(a, b)
# This method is used to mitigate timing attacks
return false unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res.zero?
end
end
Rack::Handler::Puma.run VerifyWebhook.new(secret), Port: 4567