module Stripe::Webhook::Signature

Constants

EXPECTED_SCHEME

Public Class Methods

verify_header(payload, header, secret, tolerance: nil) click to toggle source

Verifies the signature header for a given payload.

Raises a SignatureVerificationError in the following cases:

  • the header does not match the expected format

  • no signatures found with the expected scheme

  • no signatures matching the expected signature

  • a tolerance is provided and the timestamp is not within the tolerance

Returns true otherwise

# File lib/stripe/webhook.rb, line 52
def self.verify_header(payload, header, secret, tolerance: nil)
  begin
    timestamp, signatures =
      get_timestamp_and_signatures(header, EXPECTED_SCHEME)
  rescue StandardError
    raise SignatureVerificationError.new(
      "Unable to extract timestamp and signatures from header",
      header, http_body: payload
    )
  end

  if signatures.empty?
    raise SignatureVerificationError.new(
      "No signatures found with expected scheme #{EXPECTED_SCHEME}",
      header, http_body: payload
    )
  end

  signed_payload = "#{timestamp}.#{payload}"
  expected_sig = compute_signature(signed_payload, secret)
  unless signatures.any? { |s| Util.secure_compare(expected_sig, s) }
    raise SignatureVerificationError.new(
      "No signatures found matching the expected signature for payload",
      header, http_body: payload
    )
  end

  if tolerance && timestamp < Time.now.to_f - tolerance
    raise SignatureVerificationError.new(
      "Timestamp outside the tolerance zone (#{Time.at(timestamp)})",
      header, http_body: payload
    )
  end

  true
end

Private Class Methods

compute_signature(payload, secret) click to toggle source
# File lib/stripe/webhook.rb, line 27
def self.compute_signature(payload, secret)
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload)
end
get_timestamp_and_signatures(header, scheme) click to toggle source

Extracts the timestamp and the signature(s) with the desired scheme from the header

# File lib/stripe/webhook.rb, line 34
def self.get_timestamp_and_signatures(header, scheme)
  list_items = header.split(/,\s*/).map { |i| i.split("=", 2) }
  timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1])
  signatures = list_items.select { |i| i[0] == scheme }.map { |i| i[1] }
  [timestamp, signatures]
end