Module: AbacatePay::Webhooks
- Defined in:
- lib/abacate_pay/webhooks.rb,
lib/abacate_pay/webhooks/event.rb
Overview
Verification and parsing of inbound AbacatePay webhooks.
Webhook bodies are the least trusted input an integration handles: they arrive unauthenticated on a public endpoint. Every entry point here treats missing, malformed, and hostile input as an expected case and surfaces it as a typed SDK error, never as a raw parser or NoMethodError.
Defined Under Namespace
Classes: Event, PayloadError, SignatureError
Constant Summary collapse
- PUBLIC_KEY =
AbacatePay signs every delivery with this fixed key, published at https://docs.abacatepay.com/pages/webhooks/security and hard-coded in the Node, Python and Go samples there.
It is public and global, so it proves only that the body was not altered in transit, it does NOT prove the request came from AbacatePay, since anyone can compute a valid signature with it. Origin is authenticated by the
webhookSecretquery parameter; see verify_secret!. Use both. "t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMU" \ "go6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmU" \ "CKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1Jh" \ "NHDX9"
Class Method Summary collapse
-
.construct_event(payload:, signature:, secret: PUBLIC_KEY) ⇒ Event
Verifies a webhook signature and parses the body in one step.
-
.parse(payload) ⇒ Event
Parses a webhook payload into an Event object.
-
.valid?(payload:, signature:, secret: PUBLIC_KEY) ⇒ Boolean
Checks if a webhook signature is valid.
-
.verify!(payload:, signature:, secret: PUBLIC_KEY) ⇒ true
Verifies the
X-Webhook-Signatureheader: HMAC-SHA256 over the raw body, base64-encoded. -
.verify_secret!(received:, expected:) ⇒ true
Verifies the
webhookSecretquery parameter, which is what actually authenticates the request as coming from AbacatePay.
Class Method Details
.construct_event(payload:, signature:, secret: PUBLIC_KEY) ⇒ Event
Verifies a webhook signature and parses the body in one step.
This is the only entry point that cannot be used to act on an unauthenticated payload, and is what integrations should call.
113 114 115 116 |
# File 'lib/abacate_pay/webhooks.rb', line 113 def self.construct_event(payload:, signature:, secret: PUBLIC_KEY) verify!(payload: payload, signature: signature, secret: secret) parse(payload) end |
.parse(payload) ⇒ Event
Parses a webhook payload into an Event object.
Prefer construct_event, which refuses to parse a body it has not authenticated first.
93 94 95 96 97 98 99 100 |
# File 'lib/abacate_pay/webhooks.rb', line 93 def self.parse(payload) data = JSON.parse(payload.to_s) raise PayloadError, "Expected a JSON object, got #{data.class}" unless data.is_a?(Hash) Event.new(data) rescue JSON::ParserError => e raise PayloadError, "Malformed webhook payload: #{e.}" end |
.valid?(payload:, signature:, secret: PUBLIC_KEY) ⇒ Boolean
Checks if a webhook signature is valid.
Never raises for untrusted input, a missing header, an empty secret, or a forged signature all return false.
78 79 80 81 82 83 |
# File 'lib/abacate_pay/webhooks.rb', line 78 def self.valid?(payload:, signature:, secret: PUBLIC_KEY) verify!(payload: payload, signature: signature, secret: secret) true rescue SignatureError false end |
.verify!(payload:, signature:, secret: PUBLIC_KEY) ⇒ true
Verifies the X-Webhook-Signature header: HMAC-SHA256 over the raw body,
base64-encoded.
42 43 44 45 46 47 48 49 50 |
# File 'lib/abacate_pay/webhooks.rb', line 42 def self.verify!(payload:, signature:, secret: PUBLIC_KEY) raise SignatureError, "Missing webhook signature" if signature.nil? || signature.to_s.empty? raise SignatureError, "Missing webhook secret" if secret.nil? || secret.to_s.empty? expected = base64_hmac(secret.to_s, payload.to_s) raise SignatureError, "Invalid webhook signature" unless secure_compare(expected, signature.to_s) true end |
.verify_secret!(received:, expected:) ⇒ true
Verifies the webhookSecret query parameter, which is what actually
authenticates the request as coming from AbacatePay.
The HMAC signature alone cannot do this: it is computed with a public key.
61 62 63 64 65 66 67 |
# File 'lib/abacate_pay/webhooks.rb', line 61 def self.verify_secret!(received:, expected:) raise SignatureError, "Missing webhook secret parameter" if received.nil? || received.to_s.empty? raise SignatureError, "Missing expected webhook secret" if expected.nil? || expected.to_s.empty? raise SignatureError, "Invalid webhook secret" unless secure_compare(expected.to_s, received.to_s) true end |