Module: FluvPay::Webhooks
- Defined in:
- lib/fluvpay/webhooks.rb
Overview
Verificação de assinatura de webhooks da FluvPay.
A FluvPay assina cada entrega com HMAC-SHA256 sobre “{timestamp}.” corpo_cru+, usando o segredo whsec_... do webhook. O header X-FluvPay-Signature traz v1=<hex>. A verificação usa comparação em tempo constante e exige o corpo CRU (a string exatamente como recebida), nunca reserializado.
Defined Under Namespace
Classes: Event
Constant Summary collapse
- EVENT_HEADER =
"X-FluvPay-Event"- TIMESTAMP_HEADER =
"X-FluvPay-Timestamp"- DELIVERY_ID_HEADER =
"X-FluvPay-Delivery-Id"- SIGNATURE_HEADER =
"X-FluvPay-Signature"- EVENT_TYPES =
Eventos disponíveis (8). Espelha o catálogo do contrato OpenAPI.
%w[ charge.created charge.paid charge.expired charge.cancelled charge.refunded payout.created payout.completed payout.failed ].freeze
Class Method Summary collapse
-
.compute_signature(secret, timestamp, raw_body) ⇒ String
Recalcula o hex da assinatura: HMAC_SHA256(secret, timestamp + “.” + corpo_cru).
-
.extract_v1(signature_header) ⇒ Object
Extrai o hex após
v1=(aceita vários esquemas separados por vírgula). -
.secure_compare(expected, provided) ⇒ Object
Comparação de strings em tempo constante, resistente a timing attacks.
-
.verify_signature(payload, signature_header, timestamp, secret, tolerance_seconds: nil, event_type: nil, delivery_id: nil) ⇒ FluvPay::Webhooks::Event
Verifica a assinatura de um webhook e devolve o evento parseado.
Class Method Details
.compute_signature(secret, timestamp, raw_body) ⇒ String
Recalcula o hex da assinatura: HMAC_SHA256(secret, timestamp + “.” + corpo_cru).
62 63 64 65 |
# File 'lib/fluvpay/webhooks.rb', line 62 def compute_signature(secret, , raw_body) signed_payload = "#{}.#{raw_body}" OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("SHA256"), secret.to_s, signed_payload) end |
.extract_v1(signature_header) ⇒ Object
Extrai o hex após v1= (aceita vários esquemas separados por vírgula).
117 118 119 120 121 122 123 124 125 |
# File 'lib/fluvpay/webhooks.rb', line 117 def extract_v1(signature_header) return nil if signature_header.nil? || signature_header.empty? signature_header.split(",").each do |part| item = part.strip return item[3..].to_s.strip if item.start_with?("v1=") end nil end |
.secure_compare(expected, provided) ⇒ Object
Comparação de strings em tempo constante, resistente a timing attacks.
147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/fluvpay/webhooks.rb', line 147 def secure_compare(expected, provided) a = expected.to_s.b b = provided.to_s.b return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) rescue StandardError # Fallback puro Ruby caso fixed_length_secure_compare não esteja disponível. bytes = a.unpack("C*") res = 0 b.unpack("C*").each_with_index { |byte, i| res |= byte ^ bytes[i] } res.zero? end |
.verify_signature(payload, signature_header, timestamp, secret, tolerance_seconds: nil, event_type: nil, delivery_id: nil) ⇒ FluvPay::Webhooks::Event
Verifica a assinatura de um webhook e devolve o evento parseado.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/fluvpay/webhooks.rb', line 80 def verify_signature(payload, signature_header, , secret, tolerance_seconds: nil, event_type: nil, delivery_id: nil) provided = extract_v1(signature_header.to_s) if provided.nil? || provided.empty? raise SignatureVerificationError.new( "Assinatura ausente ou em formato inválido (esperado 'v1=<hex>')." ) end check_tolerance!(, tolerance_seconds) unless tolerance_seconds.nil? expected = compute_signature(secret, , payload) unless secure_compare(expected, provided) raise SignatureVerificationError.new("Assinatura do webhook não confere.") end parsed = parse_json(payload) resolved_type = event_type || (parsed.is_a?(Hash) ? parsed["event"] : nil) data = if parsed.is_a?(Hash) && parsed["data"].is_a?(Hash) parsed["data"] elsif parsed.is_a?(Hash) parsed else {} end Event.new( type: resolved_type, delivery_id: delivery_id, timestamp: , data: data, raw: parsed.is_a?(Hash) ? parsed : {} ) end |