honojs / examples

Examples using Hono.
https://hono.dev
586 stars 46 forks source link

Stripe Verification Fails in Stripe Webhook Example (Deno, Supabase Functions) #159

Closed greenstick closed 2 weeks ago

greenstick commented 2 weeks ago

First off, have to say I've really been enjoying working with Hono. It's delivered on everything I've asked of it so far and has done so with ease, with this issue being the only exception (and possibly not due to Hono at all).

The issue

In the Stripe Webhook Example docs, when I use the code as defined, the Stripe Verification function fails. This seems to be an issue with the request body, though, the error I get states it's to do with the signature (seems the error's a bit coarse – I've verified that there's no trailing whitespace in the signature, debug attempts detailed below). Here's the full error:

⚠️  Webhook signature verification failed. No signatures 
found matching the expected signature for payload. Are 
you passing the raw request body you received from Stripe? 
If a webhook request is being forwarded by a third-party tool, 
ensure that the exact request body, including JSON 
formatting and new line style, is preserved.

Learn more about webhook signing and explore webhook 
integration examples for various frameworks at 
https://github.com/stripe/stripe-node#webhook-signing

Note: The provided signing secret contains whitespace. 
This often indicates an extra newline or space is in the value

Attempts to Debug

I've tried a variety of things to get the verification working:

Likely Cause

This seems to be an issue with how Hono is processing the body of the request. If others can get this working in a different runtime, however, it may be that the Deno runtime of Supabase Functions is modifying the body (from what I've seen in my other edge functions, this isn't the case. Still, I haven't completely ruled it out).

All said, how can I get Stripe Webhook Verification working? And is the example in the docs current or does it need to be updated?

Any insights appreciated – thanks!


In case it helps:

Code

Version Information

Relevant entries from import_map.json.

"imports": {
   "@deno/server": "https://deno.land/std@0.215.0/http/server.ts",
   "@hono": "https://esm.sh/v135/hono@4.4.10",
   "@stripe": "https://esm.sh/stripe@16.1.0?target=deno"
}

Webhooks Supabase Edge Function (index.ts)

import { Hono } from "@hono";
// All Middleware Imports Removed

import type { Context } from "@hono";

// Routes
import stripe from './routes/stripe.ts';

/*
Get Mode
*/

const mode = Deno.env.get("MODE");

/*
Initialize API
*/

const api = new Hono({ strict: true }).basePath("/webhooks");

/*
Initialize Middlewares
*/

// All Middleware Instantiations Removed

/*
Set Controller Routes
*/

api.route("/stripe", stripe);

// Serve API
Deno.serve(api.fetch);

Stripe Webhook (/routes/stripe.ts)

import { Hono } from '@hono';
import { Stripe } from "@stripe";

import type { Context } from "@hono";

// Initialize Hono Routes
const routes = new Hono();

routes.on("POST", "/event/payment-intent-state-change", async (context: Context) => {

  const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") ?? "";
  const STRIPE_API_VERSION = Deno.env.get("STRIPE_API_VERSION") ?? "2023-10-16";
  const STRIPE_WEBHOOK_SECRET = Deno.env.get("STRIPE_PAYMENT_INTENT_WEBHOOK_SECRET") ?? "";
  const stripe = new Stripe(STRIPE_SECRET_KEY, {
    apiVersion: STRIPE_API_VERSION,
    httpClient: Stripe.createFetchHttpClient(),
  });

  try {

    const body = await context.req.text();
    const signature = context.req.header('stripe-signature');

    if (!signature) {
      return context.text('Status: Webhook Error – Signature not found', 400);
    }

    if (!body) {
      return context.text('Status: Webhook Error – Event not found', 400);
    }

    const event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      STRIPE_WEBHOOK_SECRET,
      undefined,
      Stripe.createSubtleCryptoProvider(),
    );

    // Handle the event
    switch (event.type) {
      case 'payment_intent.succeeded': {
        const paymentIntent = event.data.object;
        console.log(`Status: Payload Verified – PaymentIntent for ${paymentIntent.amount} was successful!`);
        // Then define and call a method to handle the successful payment intent.
        // handlePaymentIntentSucceeded(paymentIntent);
        break;
      }
      default: {
        // Unexpected event type
        console.log(`Status: Webhook Error – Unhandled event type ${event.type}.`);
      }
    }

  } catch (error) {

    const message = error instanceof Error ? error.message : 'Internal server error';
    return context.text(`Status: Webhook Error – ${message}`, 400);

  }

  // Return a 200 response to acknowledge receipt of the event
  return context.text('Status: Webhook Success', 200);

});

export default routes;
yusukebe commented 2 weeks ago

Hi @greenstick

Is this issue for the website (https://hono.dev), right ?

@hideokamoto Can you take a look?

greenstick commented 2 weeks ago

Hi @greenstick

Is this issue for the website (https://hono.dev), right ?

@hideokamoto Can you take a look?

Yes – it's possible that it may also affect Hono if it has something to do with Hono manipulating the raw body.

greenstick commented 2 weeks ago

Solved

I finally traced the issue and it wasn't to do with Hono at all. It seems that some trailing whitespace was included when copying and pasting the Stripe Webhook signing secret (i.e. the one that looks like whsec_c29Kb1aXylHno...) from the Stripe Dashboard to the Supabase Edge Function Secrets Management. Once that was fixed, it worked.