Module: BellaBaxter::WebhookSignature

Defined in:
lib/bella_baxter/webhook_signature.rb

Overview

Verifies the X-Bella-Signature header on incoming Bella Baxter webhook requests.

Signature format: X-Bella-Signature: t=unix_epoch_seconds,v1=hmac_sha256_hex Signing input: “t.rawBodyJson” (UTF-8) HMAC key: the raw signing secret string (full whsec-xxx value, UTF-8 encoded)

Constant Summary collapse

DEFAULT_TOLERANCE =
300

Class Method Summary collapse

Class Method Details

.verify(secret:, signature_header:, raw_body:, tolerance: DEFAULT_TOLERANCE) ⇒ Boolean

Verifies the X-Bella-Signature header on an incoming webhook.

Parameters:

  • secret (String)

    the whsec-xxx signing secret

  • signature_header (String)

    value of X-Bella-Signature header

  • raw_body (String)

    raw request body string (UTF-8)

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds (default 300)

Returns:

  • (Boolean)

    true if the signature is valid and the timestamp is within tolerance

Raises:



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/bella_baxter/webhook_signature.rb', line 22

def self.verify(secret:, signature_header:, raw_body:, tolerance: DEFAULT_TOLERANCE)
  parts   = signature_header.to_s.split(",")
  t_part  = parts.find { |p| p.start_with?("t=") }
  v1_part = parts.find { |p| p.start_with?("v1=") }

  raise BellaBaxter::WebhookSignatureError, "Malformed X-Bella-Signature header: missing t= or v1=" \
    if t_part.nil? || v1_part.nil?

  begin
    timestamp = Integer(t_part[2..], 10)
  rescue ArgumentError
    raise BellaBaxter::WebhookSignatureError, "Malformed X-Bella-Signature header: t= is not a valid integer"
  end

  v1  = v1_part[3..]
  age = Time.now.utc.to_i - timestamp

  raise BellaBaxter::WebhookSignatureError,
    "Webhook timestamp is stale or in the future (age=#{age}s, tolerance=#{tolerance}s)" \
    if age.abs > tolerance

  signing_input = "#{timestamp}.#{raw_body}"
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signing_input)

  secure_compare(expected, v1)
end