diff --git a/packages/payments/stripe/src/index.test.ts b/packages/payments/stripe/src/index.test.ts index b25da9e1..44c15a64 100644 --- a/packages/payments/stripe/src/index.test.ts +++ b/packages/payments/stripe/src/index.test.ts @@ -96,6 +96,33 @@ describe('payment-stripe', () => { }); }); + it('accepts any matching Stripe v1 signature when multiple are present', async () => { + const raw = JSON.stringify({ type: 'checkout.session.completed' }); + const secret = 'whsec_test'; + const timestamp = '1700000000'; + const signature = createHmac('sha256', secret) + .update(`${timestamp}.${raw}`) + .digest('hex'); + + const webhook = await adapter.verifyWebhook( + ctx({ STRIPE_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${signature},v1=${'0'.repeat(64)}`, + {}, + ); + + expect(webhook.type).toBe('checkout.session.completed'); + + const webhookWithMatchingSignatureLast = await adapter.verifyWebhook( + ctx({ STRIPE_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${'0'.repeat(64)},v1=${signature}`, + {}, + ); + + expect(webhookWithMatchingSignatureLast.type).toBe('checkout.session.completed'); + }); + it('rejects invalid Stripe webhook signatures', async () => { await expect(adapter.verifyWebhook( ctx({ STRIPE_WEBHOOK_SECRET: 'whsec_test' }), diff --git a/packages/payments/stripe/src/index.ts b/packages/payments/stripe/src/index.ts index 38e7a890..4f065c9a 100644 --- a/packages/payments/stripe/src/index.ts +++ b/packages/payments/stripe/src/index.ts @@ -162,25 +162,30 @@ async function readStripeJson(res: Response): Promise { } function verifyStripeSignature(rawBody: string, header: string, secret: string): void { - const parts = Object.fromEntries( - header.split(',').map((part) => { - const [key, value] = part.split('='); - return [key, value]; - }), - ); - const timestamp = parts.t; - const signature = parts.v1; - if (!timestamp || !signature) throw new Error('Stripe-Signature missing t or v1'); + let timestamp: string | undefined; + const signatures: string[] = []; + + for (const part of header.split(',')) { + const [key, value] = part.split('='); + if (key === 't') timestamp = value; + if (key === 'v1' && value) signatures.push(value); + } + + if (!timestamp || signatures.length === 0) throw new Error('Stripe-Signature missing t or v1'); const expected = createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); - const actualBuffer = Buffer.from(signature, 'hex'); const expectedBuffer = Buffer.from(expected, 'hex'); - if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) { - throw new Error('Invalid Stripe webhook signature'); + for (const signature of signatures) { + const actualBuffer = Buffer.from(signature, 'hex'); + if (actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)) { + return; + } } + + throw new Error('Invalid Stripe webhook signature'); } function normalizeStripeStatus(status: unknown): Webhook['status'] | undefined {