Class: Hooksmith::Verifiers::Hmac

Inherits:
Base
  • Object
show all
Defined in:
lib/hooksmith/verifiers/hmac.rb

Overview

HMAC-based webhook signature verifier.

This verifier validates webhook requests using HMAC signatures, which is a common authentication method used by providers like Stripe, GitHub, Shopify, and many others.

Examples:

Basic HMAC verification

verifier = Hooksmith::Verifiers::Hmac.new(
  secret: ENV['WEBHOOK_SECRET'],
  header: 'X-Signature'
)

HMAC with timestamp validation (like Stripe)

verifier = Hooksmith::Verifiers::Hmac.new(
  secret: ENV['STRIPE_WEBHOOK_SECRET'],
  header: 'Stripe-Signature',
  timestamp_options: { header: 'Stripe-Signature', tolerance: 300 }
)

HMAC with custom signature format

verifier = Hooksmith::Verifiers::Hmac.new(
  secret: ENV['WEBHOOK_SECRET'],
  header: 'X-Hub-Signature-256',
  algorithm: 'sha256',
  signature_prefix: 'sha256='
)

Constant Summary collapse

ALGORITHMS =

Supported HMAC algorithms

%w[sha1 sha256 sha384 sha512].freeze
DEFAULT_ENCODING =

Default signature encoding

:hex

Instance Attribute Summary

Attributes inherited from Base

#options

Instance Method Summary collapse

Constructor Details

#initialize(secret:, header:, **options) ⇒ Hmac

Initializes the HMAC verifier.

Parameters:

  • secret (String)

    the shared secret key

  • header (String)

    the header containing the signature

  • algorithm (String)

    the HMAC algorithm (sha1, sha256, sha384, sha512)

  • encoding (Symbol)

    the signature encoding (:hex or :base64)

  • signature_prefix (String, nil)

    prefix to strip from signature (e.g., ‘sha256=’)

  • timestamp_options (Hash)

    timestamp validation options



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/hooksmith/verifiers/hmac.rb', line 53

def initialize(secret:, header:, **options)
  # Extract known options and pass remaining to parent (avoids ActiveSupport dependency)
  known_keys = i[algorithm encoding signature_prefix timestamp_options]
  # rubocop:disable Style/HashExcept -- intentionally avoiding ActiveSupport's Hash#except
  parent_options = options.reject { |k, _| known_keys.include?(k) }
  # rubocop:enable Style/HashExcept
  super(**parent_options)
  @secret = secret
  @header = header
  @algorithm = validate_algorithm(options.fetch(:algorithm, 'sha256'))
  @encoding = options.fetch(:encoding, DEFAULT_ENCODING)
  @signature_prefix = options[:signature_prefix]
  configure_timestamp_options(options[:timestamp_options])
end

Instance Method Details

#enabled?Boolean

Returns whether the verifier is properly configured.

Returns:

  • (Boolean)

    true if secret and header are present



89
90
91
# File 'lib/hooksmith/verifiers/hmac.rb', line 89

def enabled?
  !@secret.nil? && !@secret.empty? && !@header.nil? && !@header.empty?
end

#verify!(request) ⇒ void

This method returns an undefined value.

Verifies the HMAC signature of the request.

Parameters:

Raises:



73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/hooksmith/verifiers/hmac.rb', line 73

def verify!(request)
  signature = extract_signature(request)
  raise VerificationError.new('Missing signature header', reason: 'missing_signature') if signature.nil?

  verify_timestamp!(request) if @timestamp_header

  expected = compute_signature(request.body)

  return if secure_compare(expected, signature)

  raise VerificationError.new('Invalid signature', reason: 'signature_mismatch')
end