Module: NakoPay::Webhook
- Defined in:
- lib/nakopay/webhook.rb
Overview
Webhook signature verifier.
NakoPay::Webhook.construct_event(raw_body, sig_header, secret)
Header format: t=<unix>,v1=<hex_hmac> Signed payload: <t>.<raw_body>
Constant Summary collapse
- DEFAULT_TOLERANCE =
seconds
300
Class Method Summary collapse
- .construct_event(payload, sig_header, secret, tolerance: DEFAULT_TOLERANCE) ⇒ Object
- .secure_compare(a, b) ⇒ Object
Class Method Details
.construct_event(payload, sig_header, secret, tolerance: DEFAULT_TOLERANCE) ⇒ Object
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 41 42 43 44 |
# File 'lib/nakopay/webhook.rb', line 16 def construct_event(payload, sig_header, secret, tolerance: DEFAULT_TOLERANCE) raise SignatureVerificationError.new("missing X-NakoPay-Signature header", code: "signature_missing") if sig_header.nil? || sig_header.empty? raise SignatureVerificationError.new("webhook secret is required", code: "secret_missing") if secret.nil? || secret.empty? parts = sig_header.split(",").each_with_object({}) do |kv, h| k, v = kv.split("=", 2) h[k.strip] = v.to_s.strip if k && v end ts = Integer(parts["t"]) rescue nil v1 = parts["v1"] raise SignatureVerificationError.new("malformed signature header", code: "signature_invalid") if ts.nil? || v1.nil? || v1.empty? now = Time.now.to_i if (now - ts).abs > tolerance raise SignatureVerificationError.new("timestamp outside tolerance window", code: "signature_timestamp_outside_tolerance") end expected = OpenSSL::HMAC.hexdigest("sha256", secret, "#{ts}.#{payload}") unless secure_compare(expected, v1) raise SignatureVerificationError.new("signature does not match expected value", code: "signature_mismatch") end begin JSON.parse(payload) rescue JSON::ParserError raise SignatureVerificationError.new("webhook payload is not valid JSON", code: "payload_invalid_json") end end |
.secure_compare(a, b) ⇒ Object
46 47 48 49 50 51 52 53 |
# File 'lib/nakopay/webhook.rb', line 46 def secure_compare(a, b) return false unless a.bytesize == b.bytesize l = a.unpack("C*") res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res.zero? end |