Class: Quolle::Resources::Webhooks

Inherits:
Object
  • Object
show all
Defined in:
lib/quolle/resources/webhooks.rb

Overview

Verify that an incoming webhook really came from Quolle.

Constant Summary collapse

DEFAULT_TOLERANCE =

seconds

300

Instance Method Summary collapse

Constructor Details

#initialize(_client = nil) ⇒ Webhooks

Returns a new instance of Webhooks.



12
# File 'lib/quolle/resources/webhooks.rb', line 12

def initialize(_client = nil); end

Instance Method Details

#verify(payload, signature_header, secret, tolerance: DEFAULT_TOLERANCE) ⇒ Hash

Verify a webhook signature and return the parsed event.

Parameters:

  • payload (String)

    the RAW request body

  • signature_header (String)

    the value of the Quolle-Signature header

  • secret (String)

    your webhook signing secret (whsec_…)

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE)

    max timestamp age in seconds (replay window)

Returns:

  • (Hash)

    the parsed event

Raises:

  • (Quolle::Error)

    on a malformed header, stale timestamp, or bad signature



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/quolle/resources/webhooks.rb', line 22

def verify(payload, signature_header, secret, tolerance: DEFAULT_TOLERANCE)
  parts = signature_header.to_s.split(",").each_with_object({}) do |seg, acc|
    k, v = seg.strip.split("=", 2)
    acc[k] = v if k && v
  end
  t = parts["t"]
  v1 = parts["v1"]
  raise Quolle::Error.new("Invalid Quolle-Signature header") if t.nil? || v1.nil? || t.empty? || v1.empty?

  ts = t.to_i
  if (Time.now.to_i - ts).abs > tolerance
    raise Quolle::Error.new("Webhook timestamp outside the tolerance window (possible replay)")
  end

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{payload}")
  unless secure_compare(expected, v1)
    raise Quolle::Error.new("Webhook signature verification failed")
  end

  JSON.parse(payload)
end