Module: Nfe::Webhook
- Defined in:
- lib/nfe/webhook.rb,
sig/nfe/webhook.rbs
Overview
Stateless helpers for verifying NFE.io webhook deliveries. This is the
canonical signature-verification API: it needs no Client, reads no
Configuration, and performs no network access — the caller supplies
the raw payload bytes, the X-Hub-Signature header value, and the secret.
IMPORTANT: pass the RAW request body bytes
NFE.io signs the exact bytes it delivered. Read the raw body BEFORE parsing
JSON (e.g. request.body.read in Rack/Rails) and pass those bytes. Do NOT
re-serialize a parsed object (+payload.to_json+) — key order and whitespace
will differ from the signed bytes and verification will fail unpredictably.
raw = request.body.read
sig = request.get_header("HTTP_X_HUB_SIGNATURE")
if Nfe::Webhook.verify_signature(payload: raw, signature: sig, secret: ENV["NFE_WEBHOOK_SECRET"])
event = Nfe::Webhook.construct_event(payload: raw, signature: sig, secret: ENV["NFE_WEBHOOK_SECRET"])
# event.id => dedupe on this; NFE.io sends no timestamp/nonce, so a valid
# signature proves authenticity but NOT freshness. Handlers MUST be
# idempotent and dedupe on the event/invoice id.
end
Only the X-Hub-Signature + HMAC-SHA1 scheme is supported. The legacy
X-NFe-Signature / HMAC-SHA256 scheme is intentionally NOT implemented; a
sha256= header is rejected.
Constant Summary collapse
- SIGNATURE_PREFIX =
Wire prefix on the
X-Hub-Signatureheader value. "sha1="- HEX_RE =
HMAC-SHA1 hex digests are always 40 lowercase hex characters.
/\A[a-f0-9]{40}\z/
Class Method Summary collapse
-
.build_event(decoded) ⇒ Object
private
Unwrap the +action+/+payload+ or +event+/+data+ envelope into a WebhookEvent; falls back to a flat +type+/+event_type+/+action+ shape carrying the whole body as
data. -
.construct_event(payload:, signature:, secret:) ⇒ Nfe::WebhookEvent
Verify, then parse and unwrap a delivery into a WebhookEvent.
- .envelope_data(decoded) ⇒ Object private
- .first_string(hash, *keys) ⇒ Object private
- .nullable_string(value) ⇒ Object private
- .parse_payload(payload) ⇒ Object private
-
.verify_signature(payload:, signature:, secret:) ⇒ Boolean
Verify an
X-Hub-Signaturevalue against the payload and secret using constant-time comparison.
Instance Method Summary collapse
- #self?.build_event ⇒ Nfe::WebhookEvent
- #self?.construct_event ⇒ Nfe::WebhookEvent
- #self?.envelope_data ⇒ Hash[untyped, untyped]
- #self?.first_string ⇒ String?
- #self?.nullable_string ⇒ String?
- #self?.parse_payload ⇒ Hash[untyped, untyped]
- #self?.verify_signature ⇒ Boolean
Class Method Details
.build_event(decoded) ⇒ 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.
Unwrap the +action+/+payload+ or +event+/+data+ envelope into a
Nfe::WebhookEvent; falls back to a flat +type+/+event_type+/+action+
shape carrying the whole body as data.
105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/nfe/webhook.rb', line 105 def build_event(decoded) type = first_string(decoded, "action", "event", "type", "event_type") data = envelope_data(decoded) Nfe::WebhookEvent.new( type: type, data: data, id: nullable_string(decoded["id"] || data["id"]), created_at: nullable_string(decoded["createdAt"] || data["createdAt"]) ) end |
.construct_event(payload:, signature:, secret:) ⇒ Nfe::WebhookEvent
Verify, then parse and unwrap a delivery into a Nfe::WebhookEvent.
79 80 81 82 83 84 85 86 |
# File 'lib/nfe/webhook.rb', line 79 def construct_event(payload:, signature:, secret:) unless verify_signature(payload: payload, signature: signature, secret: secret) raise Nfe::SignatureVerificationError, "Assinatura de webhook inválida." end decoded = parse_payload(payload) build_event(decoded) end |
.envelope_data(decoded) ⇒ 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.
118 119 120 121 |
# File 'lib/nfe/webhook.rb', line 118 def envelope_data(decoded) candidate = decoded["payload"] || decoded["data"] candidate.is_a?(Hash) ? candidate : decoded end |
.first_string(hash, *keys) ⇒ 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.
124 125 126 127 128 129 130 |
# File 'lib/nfe/webhook.rb', line 124 def first_string(hash, *keys) keys.each do |key| value = hash[key] return value if value.is_a?(String) && !value.empty? end nil end |
.nullable_string(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.
133 134 135 |
# File 'lib/nfe/webhook.rb', line 133 def nullable_string(value) value.is_a?(String) ? value : nil end |
.parse_payload(payload) ⇒ 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.
89 90 91 92 93 94 95 96 97 98 |
# File 'lib/nfe/webhook.rb', line 89 def parse_payload(payload) decoded = JSON.parse(payload.to_s) unless decoded.is_a?(Hash) raise Nfe::SignatureVerificationError, "Payload de webhook não decodificou para um objeto." end decoded rescue JSON::ParserError raise Nfe::SignatureVerificationError, "Payload de webhook não é JSON válido." end |
.verify_signature(payload:, signature:, secret:) ⇒ Boolean
Verify an X-Hub-Signature value against the payload and secret using
constant-time comparison. Returns true only on an exact HMAC-SHA1 match.
Never raises: any missing, malformed, wrong-algorithm, wrong-length, or
non-hex input yields false.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/nfe/webhook.rb', line 52 def verify_signature(payload:, signature:, secret:) return false if secret.nil? || secret.to_s.empty? signature = signature.first if signature.is_a?(Array) return false if signature.nil? signature = signature.to_s return false if signature.empty? return false unless signature[0, SIGNATURE_PREFIX.length].to_s.downcase == SIGNATURE_PREFIX received = signature[SIGNATURE_PREFIX.length..].to_s.downcase return false unless HEX_RE.match?(received) expected = OpenSSL::HMAC.hexdigest("SHA1", secret, payload.to_s) OpenSSL.secure_compare(received, expected) rescue StandardError false end |
Instance Method Details
#self?.build_event ⇒ Nfe::WebhookEvent
12 |
# File 'sig/nfe/webhook.rbs', line 12
def self?.build_event: (Hash[untyped, untyped] decoded) -> Nfe::WebhookEvent
|
#self?.construct_event ⇒ Nfe::WebhookEvent
8 |
# File 'sig/nfe/webhook.rbs', line 8
def self?.construct_event: (payload: String, signature: (String | Array[String])?, secret: String?) -> Nfe::WebhookEvent
|
#self?.envelope_data ⇒ Hash[untyped, untyped]
14 |
# File 'sig/nfe/webhook.rbs', line 14
def self?.envelope_data: (Hash[untyped, untyped] decoded) -> Hash[untyped, untyped]
|
#self?.first_string ⇒ String?
16 |
# File 'sig/nfe/webhook.rbs', line 16
def self?.first_string: (Hash[untyped, untyped] hash, *String keys) -> String?
|
#self?.nullable_string ⇒ String?
18 |
# File 'sig/nfe/webhook.rbs', line 18
def self?.nullable_string: (untyped value) -> String?
|
#self?.parse_payload ⇒ Hash[untyped, untyped]
10 |
# File 'sig/nfe/webhook.rbs', line 10
def self?.parse_payload: (untyped payload) -> Hash[untyped, untyped]
|
#self?.verify_signature ⇒ Boolean
6 |
# File 'sig/nfe/webhook.rbs', line 6
def self?.verify_signature: (payload: String, signature: (String | Array[String])?, secret: String?) -> bool
|