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)


76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/stripe/webhook.rb', line 76

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

Compute the Stripe-Signature header for a given webhook body & secret. Useful for signing payloads in unit tests.

Raises:

  • (ArgumentError)


91
92
93
94
95
96
97
98
99
100
# File 'lib/stripe/webhook.rb', line 91

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 authenticity (and recency) of a webhook, raising a SignatureVerificationError if there's a mismatch. Useful for quickly validating incoming webhooks before storing them for later processing (at which time you can use the *_without_verification methods for parsing).



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
# File 'lib/stripe/webhook.rb', line 115

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