Module: ShedCloud::PartnerApi::Webhooks

Defined in:
lib/shedcloud/partner_api/webhooks.rb

Constant Summary collapse

SIGNATURE_HEADER =
'X-ShedCloud-Signature'
EVENT_ID_HEADER =
'X-ShedCloud-Event-Id'
EVENT_TYPE_HEADER =
'X-ShedCloud-Event-Type'
DEFAULT_TOLERANCE_SECONDS =
300

Class Method Summary collapse

Class Method Details

.compute_signature(secret, timestamp, body) ⇒ Object



15
16
17
# File 'lib/shedcloud/partner_api/webhooks.rb', line 15

def compute_signature(secret, timestamp, body)
  OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{body}")
end

.verify_signature(secret, header, body, tolerance_seconds: 0) ⇒ Object



19
20
21
# File 'lib/shedcloud/partner_api/webhooks.rb', line 19

def verify_signature(secret, header, body, tolerance_seconds: 0)
  verify_signature_at(secret, header, body, Time.now.to_i, tolerance_seconds)
end

.verify_signature_at(secret, header, body, now, tolerance_seconds = 0) ⇒ Object

Raises:

  • (ArgumentError)


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
59
60
61
62
# File 'lib/shedcloud/partner_api/webhooks.rb', line 23

def verify_signature_at(secret, header, body, now, tolerance_seconds = 0)
  raise ArgumentError, 'webhook signature verification failed: secret is required' if secret.to_s.empty?
  raise ArgumentError, 'webhook signature verification failed: signature header is missing' if header.to_s.strip.empty?

  tolerance_seconds = DEFAULT_TOLERANCE_SECONDS if tolerance_seconds <= 0

  timestamp = 0
  signatures = []

  header.to_s.split(',').each do |part|
    part = part.strip
    next unless part.include?('=')

    key, value = part.split('=', 2)
    case key
    when 't'
      unless value.match?(/\A\d+\z/)
        raise ArgumentError, 'webhook signature verification failed: invalid timestamp in signature header'
      end

      timestamp = value.to_i
    when 'v1'
      signatures << value
    end
  end

  if timestamp.zero? || signatures.empty?
    raise ArgumentError, 'webhook signature verification failed: signature header missing t= or v1='
  end

  delta = (now - timestamp).abs
  if delta > tolerance_seconds
    raise ArgumentError, 'webhook signature verification failed: signature timestamp outside tolerance'
  end

  expected = compute_signature(secret, timestamp, body)
  return nil if signatures.any? { |signature| secure_compare(signature.downcase, expected) }

  raise ArgumentError, 'webhook signature verification failed: signature mismatch'
end