Module: Stripe::Webhook::Signature

Defined in:
lib/stripe/webhook.rb

Constant Summary collapse

EXPECTED_SCHEME =
"v1"

Class Method Summary collapse

Class Method Details

.compute_signature(timestamp, payload, secret) ⇒ Object

Computes a webhook signature given a time (probably the current time), a payload, and a signing secret.

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/stripe/webhook.rb', line 29

def self.compute_signature(timestamp, payload, secret)
  raise ArgumentError, "timestamp should be an instance of Time" \
    unless timestamp.is_a?(Time)
  raise ArgumentError, "payload should be a string" \
    unless payload.is_a?(String)
  raise ArgumentError, "secret should be a string" \
    unless secret.is_a?(String)

  timestamped_payload = "#{timestamp.to_i}.#{payload}"
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret,
                          timestamped_payload)
end

.generate_header(timestamp, signature, scheme: EXPECTED_SCHEME) ⇒ Object

Generates a value that would be added to a ‘Stripe-Signature` for a given webhook payload.

Note that this isn’t needed to verify webhooks in any way, and is mainly here for use in test cases (those that are both within this project and without).

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
57
# File 'lib/stripe/webhook.rb', line 48

def self.generate_header(timestamp, signature, scheme: EXPECTED_SCHEME)
  raise ArgumentError, "timestamp should be an instance of Time" \
    unless timestamp.is_a?(Time)
  raise ArgumentError, "signature should be a string" \
    unless signature.is_a?(String)
  raise ArgumentError, "scheme should be a string" \
    unless scheme.is_a?(String)

  "t=#{timestamp.to_i},#{scheme}=#{signature}"
end

.verify_header(payload, header, secret, tolerance: nil) ⇒ Object

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



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/stripe/webhook.rb', line 79

def self.verify_header(payload, header, secret, tolerance: nil)
  begin
    timestamp, signatures =
      get_timestamp_and_signatures(header, EXPECTED_SCHEME)

  # TODO: Try to knock over this blanket rescue as it can unintentionally
  # swallow many valid errors. Instead, try to validate an incoming
  # header one piece at a time, and error with a known exception class if
  # any part is found to be invalid. Rescue that class here.
  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

  expected_sig = compute_signature(timestamp, 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 - tolerance
    formatted_timestamp = Time.at(timestamp).strftime("%F %T")
    raise SignatureVerificationError.new(
      "Timestamp outside the tolerance zone (#{formatted_timestamp})",
      header, http_body: payload
    )
  end

  true
end