Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/payments/stripe/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.

it('rejects invalid Stripe webhook signatures', async () => {
await expect(adapter.verifyWebhook(
ctx({ STRIPE_WEBHOOK_SECRET: 'whsec_test' }),
Expand Down
29 changes: 17 additions & 12 deletions packages/payments/stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,30 @@ async function readStripeJson<T>(res: Response): Promise<T> {
}

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 {
Expand Down