Skip to content

Instantly share code, notes, and snippets.

@ksmandersen
Created December 3, 2019 09:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ksmandersen/ef795c95d2a61503e4fc97f0ee4fa178 to your computer and use it in GitHub Desktop.
Save ksmandersen/ef795c95d2a61503e4fc97f0ee4fa178 to your computer and use it in GitHub Desktop.
Stripe Signature Validation for Webhooks
/// https://stripe.com/docs/webhooks/signatures#verify-manually
final class StripeSignatureChecker {
// 3 days
static let tolerance: TimeInterval = 3 * 24 * 60 * 60
static func isValid(req: Request) throws -> Bool {
guard let header = req.http.headers["Stripe-Signature"].first else {
throw StripeWebhookError.missingSignatureHeader
}
// Split the header, using the , character as the separator, to get a list of elements
let components = header.split(separator: ",")
var args = [String: String]()
// Then split each element, using the = character as the separator, to get a prefix and value pair.
components.forEach { component in
let pair = component.split(separator: "=")
guard pair.count == 2 else { return }
args[String(pair[0])] = String(pair[1])
}
// The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature(s). You can discard all other elements.
guard let t = args["t"], let v1 = args["v1"] else {
return false
}
guard let data = req.http.body.data,
let body = String(data: data, encoding: .utf8) else {
throw StripeWebhookError.missingSignatureBody
}
let signedPayload = "\(t).\(body)"
guard let signedData = signedPayload.data(using: .utf8) else {
throw StripeWebhookError.unableToGenerateSignedPayload
}
guard let signingSecret = Environment.get(EnvironmentKey.Stripe.signingSecret) else {
throw StripeWebhookError.missingSigningSecret
}
// Compute an HMAC with the SHA256 hash function.
// Use the endpoint’s signing secret as the key, and use the signed_payload
// string as the message.
let digestData = try HMAC.SHA256.authenticate(signedData, key: signingSecret)
guard digestData.hexEncodedString() == v1 else {
return false
}
guard let timestamp = TimeInterval(t) else {
return false
}
// Compare the signature(s) in the header to the expected signature.
// If a signature matches, compute the difference between the current timestamp
// and the received timestamp, then decide if the difference is within your tolerance.
return Date().timeIntervalSince1970 - timestamp < tolerance
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment