Class: Wajub::Webhooks

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

Constant Summary collapse

DEFAULT_TOLERANCE =
300

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret) ⇒ Webhooks

Returns a new instance of Webhooks.



14
15
16
# File 'lib/wajub/webhooks.rb', line 14

def initialize(secret)
  @secret = secret.to_s
end

Instance Attribute Details

#secretObject (readonly)

Returns the value of attribute secret.



12
13
14
# File 'lib/wajub/webhooks.rb', line 12

def secret
  @secret
end

Instance Method Details

#construct_event(payload, signature, timestamp, tolerance = DEFAULT_TOLERANCE) ⇒ Object

Raises:

  • (ArgumentError)


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

def construct_event(payload, signature, timestamp, tolerance = DEFAULT_TOLERANCE)
  raise ArgumentError, 'Wajub: webhook_secret is required for construct_event' if @secret.empty?
  raise WebhookSignatureVerificationError, 'Invalid webhook signature format. Expected v1={hash}.' unless signature.start_with?('v1=')
  raise ArgumentError, 'Wajub: payload must be the raw request body as a String, not a parsed object' unless payload.is_a?(String)

  body = payload
  expected = Crypto.hmac_sha256(@secret, "#{timestamp}.#{body}")
  received = signature.delete_prefix('v1=')

  raise WebhookSignatureVerificationError, 'Webhook signature verification failed.' unless Crypto.timing_safe_equal(expected, received)

  ts = Float(timestamp)
  raise WebhookSignatureVerificationError, 'Invalid webhook timestamp.' unless ts.finite?

  drift = (Time.now.to_i - ts).abs
  if tolerance.positive? && drift > tolerance
    raise WebhookSignatureVerificationError,
          "Timestamp outside tolerance zone (#{drift.to_i}s drift, allowed #{tolerance}s)."
  end

  event = JSON.parse(body)
  raise WebhookSignatureVerificationError, 'Invalid webhook payload JSON.' unless event.is_a?(Hash)

  event
end