Class: Assinafy::Support::WebhookVerifier

Inherits:
Object
  • Object
show all
Defined in:
lib/assinafy/support/webhook_verifier.rb

Overview

Defensive helper for verifying webhook deliveries when your gateway or proxy signs the body with an HMAC-SHA256 secret.

The Assinafy v1 API itself does not currently document a request-signing scheme for webhook deliveries, so this class is opt-in: construct it with a secret only if you have one configured in front of your webhook receiver (e.g. via API Gateway / Cloudflare). HMAC verifies authenticity, not freshness; the receiver must separately reject replayed event IDs or enforce its gateway's timestamp policy.

Examples:

Verify and dispatch a webhook

verifier = Assinafy::Support::WebhookVerifier.new(ENV['WEBHOOK_SECRET'])
raw_body = request.body.read
# NOTE: the header below is one YOUR gateway injects (e.g. Cloudflare /
# API Gateway). Assinafy v1 does not send a signature header itself.
if verifier.verify(raw_body, request.headers['X-Webhook-Signature'])
  event = verifier.extract_event(raw_body)
  verifier.event_type(event)    # => "assignment_created"
  verifier.event_payload(event) # => { "user_name" => "John", ... } (or nil)
  verifier.event_object(event)  # => { "id" => "doc2", "type" => "Document", ... }
  verifier.event_subject(event) # => { "id" => "...", "type" => "User", ... }
end

Instance Method Summary collapse

Constructor Details

#initialize(webhook_secret = nil) ⇒ WebhookVerifier

Returns a new instance of WebhookVerifier.

Parameters:

  • webhook_secret (String, nil) (defaults to: nil)

    shared secret. When nil/empty, #verify always returns false (safe-by-default).



33
34
35
# File 'lib/assinafy/support/webhook_verifier.rb', line 33

def initialize(webhook_secret = nil)
  @webhook_secret = webhook_secret
end

Instance Method Details

#event_data(event) ⇒ Hash

Deprecated.

Prefer #event_payload (event params) and #event_object (acted-on entity). The Assinafy envelope has no top-level data key; this returns payload and falls back to object for convenience.

Parameters:

  • event (Hash, nil)

Returns:

  • (Hash)


122
123
124
125
126
# File 'lib/assinafy/support/webhook_verifier.rb', line 122

def event_data(event)
  return {} unless event.is_a?(Hash)

  event['payload'] || event['object'] || {}
end

#event_object(event) ⇒ Hash

The entity the event acted on (the documented top-level object), e.g. the Document. Includes a type discriminator.

Parameters:

  • event (Hash, nil)

Returns:

  • (Hash)


99
100
101
102
103
# File 'lib/assinafy/support/webhook_verifier.rb', line 99

def event_object(event)
  return {} unless event.is_a?(Hash)

  event['object'] || {}
end

#event_payload(event) ⇒ Hash?

The event-specific data snapshot (the documented top-level payload). May be nil for events that carry no extra params (e.g. document_uploaded).

Parameters:

  • event (Hash, nil)

Returns:

  • (Hash, nil)


88
89
90
91
92
# File 'lib/assinafy/support/webhook_verifier.rb', line 88

def event_payload(event)
  return nil unless event.is_a?(Hash)

  event['payload']
end

#event_subject(event) ⇒ Hash

The actor that triggered the event (the documented top-level subject), e.g. the User. Includes a type discriminator.

Parameters:

  • event (Hash, nil)

Returns:

  • (Hash)


110
111
112
113
114
# File 'lib/assinafy/support/webhook_verifier.rb', line 110

def event_subject(event)
  return {} unless event.is_a?(Hash)

  event['subject'] || {}
end

#event_type(event) ⇒ String?

Pull the event-type code from a parsed event Hash. The canonical key in the Assinafy v1 delivery envelope is event (e.g. assignment_created).

Examples:

verifier.event_type({ 'event' => 'document_ready' }) # => "document_ready"

Parameters:

  • event (Hash, nil)

Returns:

  • (String, nil)


76
77
78
79
80
# File 'lib/assinafy/support/webhook_verifier.rb', line 76

def event_type(event)
  return nil unless event.is_a?(Hash)

  event['event']
end

#extract_event(payload) ⇒ Hash?

Parse a JSON webhook body into a Hash, returning nil on malformed or non-object payloads.

Parameters:

  • payload (String)

Returns:

  • (Hash, nil)


61
62
63
64
65
66
67
# File 'lib/assinafy/support/webhook_verifier.rb', line 61

def extract_event(payload)
  text   = payload.is_a?(String) ? payload : payload.to_s
  parsed = JSON.parse(text)
  parsed.is_a?(Hash) ? parsed : nil
rescue JSON::ParserError
  nil
end

#verify(payload, signature) ⇒ Boolean

Constant-time compare the provided signature to the expected HMAC-SHA256 of the raw payload.

Parameters:

  • payload (String)

    raw HTTP body

  • signature (String)

    hex-encoded signature header value

Returns:

  • (Boolean)


43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/assinafy/support/webhook_verifier.rb', line 43

def verify(payload, signature)
  return false unless @webhook_secret && !@webhook_secret.empty?
  return false unless signature && !signature.to_s.strip.empty?

  body     = payload.is_a?(String) ? payload : payload.to_s
  expected = OpenSSL::HMAC.hexdigest('SHA256', @webhook_secret, body)
  provided = signature.to_s.strip

  OpenSSL.fixed_length_secure_compare(expected, provided)
rescue StandardError
  false
end