Writing unit tests for stripe webhooks stripe-signature
Asked Answered
N

7

13

I'm trying to write unit tests for Stripe webhooks. The problem is I'm also verifying the stripe-signature and it fails as expected.

Is there a way to pass a correct signature in tests to the webhook with mock data?

This is the beginning of the webhook route I'm trying to handle

// Retrieve the event by verifying the signature using the raw body and secret.
let event: Stripe.Event;
const signature = headers["stripe-signature"];

try {
  event = stripe.webhooks.constructEvent(
    raw,
    signature,
    context.env.stripeWebhookSecret
  );
} catch (err) {
  throw new ResourceError(RouteErrorCode.STRIPE_WEBHOOK_SIGNATURE_VERIFICATION_FAILD);
}

// Handle event...

And the current test I'm trying to handle, I'm using Jest:

const postData = { MOCK WEBHOOK EVENT DATA }

const result = await request(app.app)
  .post("/webhook/stripe")
  .set('stripe-signature', 'HOW TO GET THIS SIGNATURE?')
  .send(postData);
Nadabas answered 15/12, 2020 at 13:20 Comment(0)
N
4

With the help of Nolan I was able to get the signature working. In case anyone else needs help, this is what I did:

import { createHmac } from 'crypto';

const unixtime = Math.floor(new Date().getTime() / 1000);

// Calculate the signature using the UNIX timestamp, postData and webhook secret
const signature = createHmac('sha256', stripeWebhookSecret)
  .update(`${unixtime}.${JSON.stringify(postData)}`, 'utf8')
  .digest('hex');

// Set the stripe-signature header with the v1 signature
// v0 can be any value since its not used in the signature calculation
const result = await request(app.app)
  .post("/webhook/stripe")
  .set('stripe-signature', `t=${unixtime},v1=${signature},v0=ff`)
  .send(postData);
Nadabas answered 16/12, 2020 at 12:27 Comment(2)
Even this approach can resolve the problem but it should avoid because we do it by hand, should use the above answer instead.Hanes
This also works for non Stripe webhooks.Nadabas
N
22

Stripe now exposes a function in its node library that they recommend for creating signatures for testing:

Testing Webhook signing

You can use stripe.webhooks.generateTestHeaderString to mock webhook events that come from Stripe:

const payload = {
  id: 'evt_test_webhook',
  object: 'event',
};

const payloadString = JSON.stringify(payload, null, 2);
const secret = 'whsec_test_secret';

const header = stripe.webhooks.generateTestHeaderString({
  payload: payloadString,
  secret,
});

const event = stripe.webhooks.constructEvent(payloadString, header, secret);

// Do something with mocked signed event
expect(event.id).to.equal(payload.id);

ref: https://github.com/stripe/stripe-node#webhook-signing

Niki answered 3/8, 2021 at 11:13 Comment(1)
This is the best answer because it comes from official docs, and we don't need to build the signature by our hand which can break our tests when Stripe changes the internal way to build signature.Hanes
N
4

With the help of Nolan I was able to get the signature working. In case anyone else needs help, this is what I did:

import { createHmac } from 'crypto';

const unixtime = Math.floor(new Date().getTime() / 1000);

// Calculate the signature using the UNIX timestamp, postData and webhook secret
const signature = createHmac('sha256', stripeWebhookSecret)
  .update(`${unixtime}.${JSON.stringify(postData)}`, 'utf8')
  .digest('hex');

// Set the stripe-signature header with the v1 signature
// v0 can be any value since its not used in the signature calculation
const result = await request(app.app)
  .post("/webhook/stripe")
  .set('stripe-signature', `t=${unixtime},v1=${signature},v0=ff`)
  .send(postData);
Nadabas answered 16/12, 2020 at 12:27 Comment(2)
Even this approach can resolve the problem but it should avoid because we do it by hand, should use the above answer instead.Hanes
This also works for non Stripe webhooks.Nadabas
B
2

If you are in ruby you can do:

      Stripe::Webhook::Signature.generate_header(
        Time.now,
        Stripe::Webhook::Signature.compute_signature(
          Time.now,
          payload,
          secret
        )
      )
Barmen answered 16/6, 2022 at 13:28 Comment(0)
J
2

If you're using Python, you can create the signature header like this:

import time
import stripe

def generate_header(payload: str, secret: str) -> str:
    timestamp = int(time.time())
    scheme = stripe.WebhookSignature.EXPECTED_SCHEME
    payload_to_sign = f"{timestamp}.{payload}"
    signature = stripe.WebhookSignature._compute_signature(payload_to_sign, secret)
    return f"t={timestamp},{scheme}={signature}"

Jyoti answered 31/10, 2023 at 12:34 Comment(2)
Can you give an example of working payload?Formative
The payload doesn't really matter. It can be anything.Jyoti
N
0

I landed here trying to setup an integration test in Rails, and just to add to Niels Kristian's answer with a bit more surrounding code I landed on the following (rework the params as needed, and add the actual STRIPE_ENDPOINT_SECRET):

def post_stripe_webhook
  params = {
    type: 'checkout.session.completed',
    data: {
      object: {
        id:             Faker::Internet.uuid,
        metadata:       {
          request_payment_token: Faker::Internet.uuid
        },
        payment_status: 'paid',
        payment_link:   'plink_dfdasfasdff23fsdf2esd'
      }
    }
  }
  stripe_header = Stripe::Webhook::Signature.generate_header(
    Time.now,
    Stripe::Webhook::Signature.compute_signature(Time.now, JSON.dump(params), 'STRIPE_ENDPOINT_SECRET')
  )
  headers = { 'HTTP_STRIPE_SIGNATURE': stripe_header }
  post '/api/webhooks/payments/successful', params:, headers:, as: :json
end
Nectarine answered 22/1 at 7:52 Comment(0)
G
0

What worked for me:

describe('Stripe webhooks', () => {
    it('handles payment_intent.succeeded event', async () => {
        const paymentIntent = {
            id: 'pi_456',
            amount: 2000,
            // ..etc etc
        }

        const event = {
            id: 'evt_1',
            type: 'payment_intent.succeeded',
            data: { object: paymentIntent },
        }
        const payloadString = JSON.stringify(event)

        const header = Stripe.webhooks.generateTestHeaderString({
            payload: payloadString,
            secret: 'whsec_lorim_ipsum_etc_etc',
        })

        // endpoint of your running test server
        const response = await axios.post(`${SERVER_END_POINT}/webhook/stripe`, event, {
            headers: {
                'stripe-signature': header,
            },
        })

        expect(response.status).toBe(200)
        // etc..
    })
})

This requires installing and importing a stripe js lib:

import Stripe from 'stripe'

The handling code can be taken from here https://stripe.com/docs/webhooks/quickstart

However do note, if you parse JSON with your express server you must add this webhook route to your server BEFORE any json parsing libs (eg app.use(express.json()) or app.use(bodyParser.json({ limit: '50mb' })) for instance).

Or alternatively instruct express to treat the webhook route differently: app.use('/webhook/stripe', express.raw({ type: 'application/json' }));

It was also useful during development/testing to use the CLI.

first run this to intercept webhook traffic and route it to your local app: stripe listen --forward-to localhost:4001/webhook/stripe

and then run stripe trigger invoice.payment_succeeded to have test webhook events fired off.

Glottic answered 31/1 at 14:12 Comment(0)
G
0

Here is how you generate a mock stripe signature header in Java:

/* 
 * Format the computed signature as a Stripe header
 */
private static String makeSigHeader(String signature) {
        return "t=%d,v1=%s,v0=abc123".formatted(getTimeNow(), signature);
    }

    /**
     * https://github.com/stripe/stripe-java/blob/master/src/main/java/com/stripe/net/Webhook.java
     * Computes the signature for a given payload and secret.
     *
     * <p>
     * The current scheme used by Stripe ("v1") is HMAC/SHA-256.
     *
     * @param payload the payload to sign.
     * @param secret  the secret used to generate the signature.
     * @return the signature as a string.
     */
    private static String computeSignature(String payload, String secret)
            throws NoSuchAlgorithmException, InvalidKeyException {
        return computeHmacSha256(secret, payload);
    }

    /**
     * Computes the HMAC/SHA-256 code for a given key and message.
     *
     * @param key     the key used to generate the code.
     * @param message the message.
     * @return the code as a string.
     */
    public static String computeHmacSha256(String key, String message)
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac hasher = Mac.getInstance("HmacSHA256");
        hasher.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = hasher.doFinal(message.getBytes(StandardCharsets.UTF_8));
        String result = "";
        for (byte b : hash) {
            result += Integer.toString((b & 0xff) + 0x100, 16).substring(1);
        }
        return result;
    }

    /**
     * Returns the current UTC timestamp in seconds.
     *
     * @return the timestamp as a long.
     */
    public static long getTimeNow() {
        long time = System.currentTimeMillis() / 1000L;
        return time;
    }

This is from the stripe-java source code: https://github.com/stripe/stripe-java/blob/master/src/main/java/com/stripe/net/Webhook.java

Galligaskins answered 12/4 at 16:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.