Class: Hooks::Plugins::Auth::HMAC
- Defined in:
- lib/hooks/plugins/auth/hmac.rb
Overview
Generic HMAC signature validator for webhooks
This validator supports multiple webhook providers with different signature formats. It provides flexible configuration options to handle various HMAC-based authentication schemes.
Constant Summary collapse
- MAX_SIGNATURE_LENGTH =
Security constants
ENV.fetch("HOOKS_MAX_SIGNATURE_LENGTH", 1024).to_i
- DEFAULT_CONFIG =
Note:
These values provide sensible defaults for most webhook implementations
Default configuration values for HMAC validation
{ algorithm: "sha256", format: "algorithm=signature", # Format: algorithm=hash header: "X-Signature", # Default header containing the signature timestamp_tolerance: 300, # 5 minutes tolerance for timestamp validation version_prefix: "v0", # Default version prefix for versioned signatures header_format: "simple" # Header format: "simple" or "structured" }.freeze
- FORMATS =
Note:
Supports three common webhook signature formats:
-
algorithm=signature: “sha256=abc123…” (GitHub, GitLab style)
-
signature_only: “abc123…” (Shopify style)
-
version=signature: “v0=abc123…” (Slack style)
Mapping of signature format strings to internal format symbols
-
{ "algorithm=signature" => :algorithm_prefixed, # "sha256=abc123..." "signature_only" => :hash_only, # "abc123..." "version=signature" => :version_prefixed # "v0=abc123..." }.freeze
Constants inherited from Base
Base::MAX_HEADER_VALUE_LENGTH, Base::MAX_PAYLOAD_SIZE
Class Method Summary collapse
-
.valid?(payload:, headers:, config:) ⇒ Boolean
Validate HMAC signature from webhook requests.
Methods inherited from Base
fetch_secret, find_header_value, timestamp_validator, valid_header_value?, valid_headers?, valid_payload_size?
Methods included from Core::ComponentAccess
#failbot, #log, #method_missing, #respond_to_missing?, #stats
Class Method Details
.valid?(payload:, headers:, config:) ⇒ Boolean
This method is designed to be safe and will never raise exceptions
Uses Rack::Utils.secure_compare to prevent timing attacks
Validate HMAC signature from webhook requests
Performs comprehensive HMAC signature validation with support for multiple signature formats and optional timestamp validation. Uses secure comparison to prevent timing attacks.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/hooks/plugins/auth/hmac.rb', line 109 def self.valid?(payload:, headers:, config:) # fetch the required secret from environment variable as specified in the config secret = fetch_secret(config) validator_config = build_config(config) # Security: Check raw headers and payload BEFORE processing return false unless valid_headers?(headers) return false unless valid_payload_size?(payload) signature_header = validator_config[:header] # Find the signature header with case-insensitive matching raw_signature = find_header_value(headers, signature_header) if raw_signature.nil? || raw_signature.empty? log.warn("Auth::HMAC validation failed: Missing or empty signature header '#{signature_header}'") return false end # Validate signature format using shared validation but with HMAC-specific length limit return false unless validate_signature_format?(raw_signature) # Now we can safely normalize headers for the rest of the validation normalized_headers = normalize_headers(headers) # Handle structured headers (e.g., Tailscale format: "t=123,v1=abc") if validator_config[:header_format] == "structured" parsed_signature_data = parse_structured_header(raw_signature, validator_config) if parsed_signature_data.nil? log.warn("Auth::HMAC validation failed: Could not parse structured signature header") return false end provided_signature = parsed_signature_data[:signature] # For structured headers, timestamp comes from the signature header itself if parsed_signature_data[:timestamp] normalized_headers = normalized_headers.merge( "extracted_timestamp" => parsed_signature_data[:timestamp] ) # Override timestamp_header to use our extracted timestamp validator_config = validator_config.merge(timestamp_header: "extracted_timestamp") end else provided_signature = normalized_headers[signature_header.downcase] end # Validate timestamp if required (for services that include timestamp validation) # It should be noted that not all HMAC implementations require timestamp validation, # so this is optional based on configuration. if validator_config[:timestamp_header] unless (normalized_headers, validator_config) log.warn("Auth::HMAC validation failed: Invalid timestamp") return false end end # Compute expected signature computed_signature = compute_signature( payload:, headers: normalized_headers, secret:, config: validator_config ) # Use secure comparison to prevent timing attacks result = Rack::Utils.secure_compare(computed_signature, provided_signature) if result log.debug("Auth::HMAC validation successful for header '#{signature_header}'") else log.warn("Auth::HMAC validation failed: Signature mismatch") end result rescue StandardError => e log.error("Auth::HMAC validation failed: #{e.}") false end |