Module: Sendara::Webhooks
- Defined in:
- lib/sendara/webhooks.rb
Constant Summary collapse
- SIGNATURE_HEADER =
"Sendara-Signature"- TIMESTAMP_HEADER =
"Sendara-Timestamp"- EVENT_ID_HEADER =
"Sendara-Event-Id"- EVENT_TYPE_HEADER =
"Sendara-Event-Type"
Class Method Summary collapse
- .header_value(headers, name) ⇒ Object
- .scalar_header(value) ⇒ Object
- .secure_compare(expected, provided) ⇒ Object
- .sign(secret, timestamp, raw_body) ⇒ Object
- .verify(payload, headers, secret, tolerance: 300) ⇒ Object
Class Method Details
.header_value(headers, name) ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/sendara/webhooks.rb', line 54 def header_value(headers, name) return nil if headers.nil? if headers.respond_to?(:[]) && !headers.is_a?(Array) direct = headers[name] || headers[name.downcase] || headers[name.upcase] return scalar_header(direct) unless direct.nil? end lower = name.downcase headers.each do |key, value| return scalar_header(value) if key.to_s.downcase == lower end nil end |
.scalar_header(value) ⇒ Object
69 70 71 72 |
# File 'lib/sendara/webhooks.rb', line 69 def scalar_header(value) value = value.first if value.is_a?(Array) value.nil? ? nil : value.to_s end |
.secure_compare(expected, provided) ⇒ Object
46 47 48 49 50 51 52 |
# File 'lib/sendara/webhooks.rb', line 46 def secure_compare(expected, provided) expected_bytes = expected.to_s.b provided_bytes = provided.to_s.b return false unless expected_bytes.bytesize == provided_bytes.bytesize OpenSSL.fixed_length_secure_compare(expected_bytes, provided_bytes) end |
.sign(secret, timestamp, raw_body) ⇒ Object
42 43 44 |
# File 'lib/sendara/webhooks.rb', line 42 def sign(secret, , raw_body) OpenSSL::HMAC.hexdigest("SHA256", secret, "#{}.#{raw_body}") end |
.verify(payload, headers, secret, tolerance: 300) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/sendara/webhooks.rb', line 15 def verify(payload, headers, secret, tolerance: 300) raise WebhookVerificationError, "A signing secret is required" if secret.nil? || secret.empty? raw_body = payload.to_s signature = header_value(headers, SIGNATURE_HEADER) = header_value(headers, TIMESTAMP_HEADER) raise WebhookVerificationError, "Missing #{SIGNATURE_HEADER} header" if signature.nil? || signature.empty? raise WebhookVerificationError, "Missing #{TIMESTAMP_HEADER} header" if .nil? || .empty? if tolerance.positive? raise WebhookVerificationError, "Invalid timestamp header" unless .match?(/\A-?\d+\z/) skew = (Time.now.to_i - .to_i).abs raise WebhookVerificationError, "Timestamp outside tolerance window" if skew > tolerance end expected = sign(secret, , raw_body) raise WebhookVerificationError, "Signature mismatch" unless secure_compare(expected, signature) begin JSON.parse(raw_body) rescue JSON::ParserError raise WebhookVerificationError, "Body is not valid JSON" end end |