'Stripe Webhook Verification Error with Play Framework

I am trying to set up stripe payment with a play framework application. I am having issues with setting up the webhook.

com.stripe.exception.SignatureVerificationException: No signatures found matching the expected signature for payload

This is the error I keep getting when I try and construct the event sent from stripe. I print out the values for the body and the signature and they look like they should be correct. This is the code I am using to collect the webhook.

 def webhook = Action { implicit request: Request[AnyContent] =>
println(request.headers.toMap)
val bodyOp:Option[JsValue] = request.body.asJson
val sigOp:Option[String] = request.headers.get("Stripe-Signature")
var event: Event = null
if (bodyOp.isEmpty || sigOp.isEmpty) {
  WebhookController.logger.write("EMPTY BODY OR SIG body-"+bodyOp+"   sig-"+sigOp,Logger.RED)
  BadRequest
} else {
  val body = bodyOp.get.toString
  val sig = sigOp.get
  println(body)
  println(sig)
  try {
    event = Webhook.constructEvent(body, sig, "whsec_5XwS8yCNOcq1CKfhh2Dtvm8RaoaE3p7b")
    val eventType: String = event.getType
    eventType match {
      case "customer.subscription.deleted" => deleteCustomer(event)
      case "invoice.payment.succeeded" => successPayment(event)
      case "invoice.payment.failed" => failedPayment(event)
      case _ => WebhookController.logger.write("UNKNOWN " + event, Logger.RED)
    }

    Ok("")
  } catch {
    case e: JsonSyntaxException =>
      e.printStackTrace()
      WebhookController.logger.write("ERROR" + e.getMessage +" "+exceptionToString(e), Logger.RED)
      BadRequest
    case e: SignatureVerificationException =>
      e.printStackTrace()
      WebhookController.logger.write("ERROR" + e.getMessage + " "+exceptionToString(e), Logger.RED)
      WebhookController.logger.write("SIG ERROR header-"+e.getSigHeader+" status code-"+e.getStatusCode,Logger.RED )
      BadRequest
  }
}
}


Solution 1:[1]

Karllekko was on the right track. Play framework automatically parsed it as a json which caused the error. request.body.asText didn't work because the content-type header value was set to json. Tolarant Text would have worked except for stripe sends their webhooks with utf-8 and tolarant text doesn't parse with utf-8. So I ended up having to use a RawBuffer and turning that into a String (https://www.playframework.com/documentation/2.6.x/ScalaBodyParsers)

class WebhookController @Inject()(parsers: PlayBodyParsers) extends Controller() {

def webhook = Action(parsers.raw) { implicit request: Request[RawBuffer] =>
  val bodyOp =  request.body.asBytes()
  val sigOp:Option[String] = request.headers.get("Stripe-Signature")
  var event: Event = null
  if (bodyOp.isEmpty || sigOp.isEmpty) {
    BadRequest
  } else {
    val body = bodyOp.get.utf8String
    val sig = sigOp.get
    try {
      event = Webhook.constructEvent(body, sig, secret)
      ...
      ...

Solution 2:[2]

You need to pass the raw body of the request exactly as received to constructEvent, not the JSON parsed version.

I believe that should be

event = Webhook.constructEvent(request.body.asText, sig, "whsec_5XwS8yCNOcq1CKfhh2Dtvm8RaoaE3p7b")

in this case?

You can have a look at the examples from Stripe as well[0]. But the core issue is that you must sign the exact string sent by Stripe, without any interference by your code or a framework in the middle.

[0]- https://github.com/stripe/stripe-java/blob/master/src/test/java/com/stripe/net/WebhookTest.java

Solution 3:[3]

zamsler's solution works for me. Thank you! Saved me a lot of time. For anyone who wants the Play 2.8 Java version:

@BodyParser.Of(BodyParser.Raw.class)
public Result webhook(Http.Request request) {
    String payload = request.body().asBytes().utf8String();
    String sigHeader = request.getHeaders().get("Stripe-Signature").get();
    String endpointSecret = "xxxxxxxx";
    try {
        event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
    } catch (SignatureVerificationException e) {
        e.printStackTrace();
        // Invalid signature
        return badRequest();
    }

    ...
    ...
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 karllekko
Solution 3 Nitin