Module: Blueticks::Webhooks
- Defined in:
- lib/blueticks/webhooks.rb
Overview
Webhook signature verification. Use this in your webhook handler to confirm requests really came from Blueticks before trusting the payload.
event = Blueticks::Webhooks.verify(
payload: raw_body,
headers: request.headers,
secret: ENV["BLUETICKS_WEBHOOK_SECRET"]
)
Returns a Blueticks::Types::WebhookEvent. Raises Blueticks::Errors::WebhookVerificationError on any mismatch.
Constant Summary collapse
- DEFAULT_TOLERANCE_SECONDS =
300
Class Method Summary collapse
- .extract_v1_signature(signature_raw) ⇒ Object private
- .lookup_header(headers, name) ⇒ Object private
- .normalize_header(value) ⇒ Object private
- .parse_payload(payload_str) ⇒ Object private
- .parse_timestamp(raw) ⇒ Object private
-
.secure_equal?(expected, supplied) ⇒ Boolean
private
Constant-time comparison to avoid timing leaks.
-
.verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Blueticks::Types::WebhookEvent
Verify a webhook signature and return the parsed event.
Class Method Details
.extract_v1_signature(signature_raw) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
86 87 88 89 90 91 92 |
# File 'lib/blueticks/webhooks.rb', line 86 def extract_v1_signature(signature_raw) signature_raw.split(",").each do |part| trimmed = part.strip return trimmed[3..] if trimmed.start_with?("v1=") end nil end |
.lookup_header(headers, name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/blueticks/webhooks.rb', line 58 def lookup_header(headers, name) return nil if headers.nil? direct = headers[name] || headers[name.to_sym] return normalize_header(direct) unless direct.nil? lower = name.downcase headers.each do |k, v| return normalize_header(v) if k.to_s.downcase == lower end nil end |
.normalize_header(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
72 73 74 75 76 |
# File 'lib/blueticks/webhooks.rb', line 72 def normalize_header(value) return value[0] if value.is_a?(Array) value.to_s end |
.parse_payload(payload_str) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
95 96 97 98 99 100 101 102 |
# File 'lib/blueticks/webhooks.rb', line 95 def parse_payload(payload_str) data = JSON.parse(payload_str) raise Errors::WebhookVerificationError, "invalid_signature: payload not JSON object" unless data.is_a?(Hash) data rescue JSON::ParserError raise Errors::WebhookVerificationError, "invalid_signature: payload not JSON" end |
.parse_timestamp(raw) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
79 80 81 82 83 |
# File 'lib/blueticks/webhooks.rb', line 79 def (raw) Integer(raw.to_s) rescue ArgumentError, TypeError raise Errors::WebhookVerificationError, "invalid timestamp" end |
.secure_equal?(expected, supplied) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Constant-time comparison to avoid timing leaks.
106 107 108 109 110 |
# File 'lib/blueticks/webhooks.rb', line 106 def secure_equal?(expected, supplied) return false if expected.bytesize != supplied.bytesize OpenSSL.fixed_length_secure_compare(expected, supplied) end |
.verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Blueticks::Types::WebhookEvent
Verify a webhook signature and return the parsed event.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/blueticks/webhooks.rb', line 33 def verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) = lookup_header(headers, "Blueticks-Webhook-Timestamp") signature_raw = lookup_header(headers, "Blueticks-Webhook-Signature") if .nil? || .empty? || signature_raw.nil? || signature_raw.empty? raise Errors::WebhookVerificationError, "missing required headers" end = () raise Errors::WebhookVerificationError, "expired timestamp" if (Time.now.to_i - ).abs > tolerance payload_str = payload.is_a?(String) ? payload : payload.to_s signed = "#{}.#{payload_str}" expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed) supplied = extract_v1_signature(signature_raw) raise Errors::WebhookVerificationError, "invalid_signature: missing v1 scheme" if supplied.nil? raise Errors::WebhookVerificationError, "invalid_signature: mismatch" unless secure_equal?(expected, supplied) parsed = parse_payload(payload_str) Types::WebhookEvent.from_hash(parsed) end |