Module: Increase::Webhook::Signature

Defined in:
lib/increase/webhook/signature.rb

Constant Summary collapse

DEFAULT_TIME_TOLERANCE =

300 seconds (5 minutes)

300
DEFAULT_SCHEME =
"v1"

Class Method Summary collapse

Class Method Details

.compute_signature(timestamp:, payload:, secret:) ⇒ Object

Raises:

  • (ArgumentError)
[View source]

60
61
62
63
64
65
66
67
# File 'lib/increase/webhook/signature.rb', line 60

def self.compute_signature(timestamp:, payload:, secret:)
  raise ArgumentError, "timestamp is required" if timestamp.nil?
  raise ArgumentError, "payload is required" if payload.nil?
  raise ArgumentError, "secret is required" if secret.nil?

  signed_payload = timestamp.to_s + "." + payload.to_s
  OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
end

.verify(payload:, signature_header:, secret:, scheme: DEFAULT_SCHEME, time_tolerance: DEFAULT_TIME_TOLERANCE) ⇒ Object

Raises a WebhookSignatureVerificationError if the signature is invalid

[View source]

19
20
21
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
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/increase/webhook/signature.rb', line 19

def self.verify(payload:, signature_header:, secret:, scheme: DEFAULT_SCHEME, time_tolerance: DEFAULT_TIME_TOLERANCE)
  # Helper for raising errors with additional metadata
  sig_error = ->(msg) do
    WebhookSignatureVerificationError.new(msg, signature_header: signature_header, payload: payload)
  end

  # Parse header
  sig_values = signature_header&.split(",")&.map { |pair| pair.split("=") }&.to_h || {}

  # Extract values
  t = sig_values["t"] # Should be a string (ISO-8601 timestamp)
  sig = sig_values[scheme]
  raise sig_error.call("No timestamp found in signature header") if t.nil?
  raise sig_error.call("No signature found with scheme #{scheme} in signature header") if sig.nil?

  # Check signature
  raise sig_error.call("Webhook secret is required") if secret.nil?
  raise sig_error.call("Payload is required") if payload.nil?
  expected_sig = compute_signature(timestamp: t, payload: payload, secret: secret)
  matches = Util.secure_compare(expected_sig, sig)
  raise sig_error.call("Signature mismatch") unless matches

  # Check timestamp tolerance to prevent timing attacks
  if time_tolerance > 0
    begin
      timestamp = DateTime.parse(t)
      now = DateTime.now
      diff = (now - timestamp) * 24 * 60 * 60 # in seconds

      # Don't allow timestamps in the future
      if diff > time_tolerance || diff < 0
        raise sig_error.call("Timestamp outside of the tolerance zone")
      end
    rescue Date::Error
      raise sig_error.call("Invalid timestamp in signature header: #{t}")
    end
  end

  true
end

.verify?(**args) ⇒ Boolean

Verifies the signature of a webhook payload (without raising an error)

Returns:

  • (Boolean)
[View source]

12
13
14
15
16
# File 'lib/increase/webhook/signature.rb', line 12

def self.verify?(**args)
  verify(**args)
rescue WebhookSignatureVerificationError
  false
end