Created
December 3, 2019 09:06
-
-
Save ksmandersen/ef795c95d2a61503e4fc97f0ee4fa178 to your computer and use it in GitHub Desktop.
Stripe Signature Validation for Webhooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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