-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
122 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default defineEventHandler(async (event) => { | ||
const isValidWebhook = await isValidStripeWebhook(event) | ||
|
||
if (!isValidWebhook) throw createError({ statusCode: 401, message: 'Unauthorized: webhook is not valid' }) | ||
|
||
return { isValidWebhook } | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { subtle } from 'node:crypto' | ||
import { Buffer } from 'node:buffer' | ||
import { type H3Event, getRequestHeaders, readRawBody } from 'h3' | ||
import { useRuntimeConfig } from '#imports' | ||
|
||
const DEFAULT_TOLERANCE = 300 | ||
|
||
const extractHeaders = (header: string) => { | ||
const parts = header.split(',') | ||
let t = '' | ||
let v1 = '' | ||
for (const part of parts) { | ||
const [key, value] = part.split('=') | ||
if (value) { | ||
if (key === 't') t = value | ||
else if (key === 'v1') v1 = value | ||
} | ||
} | ||
if (!(t && v1)) return null | ||
return { t: Number.parseInt(t), v1 } | ||
} | ||
|
||
/** | ||
* Validates Stripe webhooks on the Edge \ | ||
* Inspired by: https://docs.stripe.com/webhooks?verify=verify-manually | ||
* @async | ||
* @param event H3Event | ||
* @returns {boolean} `true` if the webhook is valid, `false` otherwise | ||
*/ | ||
export const isValidStripeWebhook = async (event: H3Event): Promise<boolean> => { | ||
const headers = getRequestHeaders(event) | ||
const body = await readRawBody(event) | ||
const { secretKey } = useRuntimeConfig(event).webhook.stripe | ||
|
||
const STRIPE_SIGNATURE = 'Stripe-Signature'.toLowerCase() | ||
const stripeSignature = headers[STRIPE_SIGNATURE] | ||
|
||
if (!body || !stripeSignature) return false | ||
|
||
const signatureHeaders = extractHeaders(stripeSignature) | ||
if (!signatureHeaders) return false | ||
const { t: webhookTimestamp, v1: webhookSignature } = signatureHeaders | ||
|
||
if ((new Date().getTime() / 1000) - webhookTimestamp > DEFAULT_TOLERANCE) return false | ||
|
||
const payloadWithTime = `${webhookTimestamp}.${body}` | ||
const encoder = new TextEncoder() | ||
const algorithm = { name: 'HMAC', hash: 'SHA-256' } | ||
|
||
const key = await subtle.importKey('raw', encoder.encode(secretKey), algorithm, false, ['sign']) | ||
const hmac = await subtle.sign(algorithm.name, key, encoder.encode(payloadWithTime)) | ||
|
||
const computedHash = Buffer.from(hmac).toString('hex') | ||
|
||
return computedHash === webhookSignature | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { defineEventHandler, createError } from 'h3' | ||
import { isValidStripeWebhook } from './../../../../../../src/runtime/server/utils/webhooks' | ||
|
||
export default defineEventHandler(async (event) => { | ||
const isValidWebhook = await isValidStripeWebhook(event) | ||
|
||
if (!isValidWebhook) throw createError({ statusCode: 401, message: 'Unauthorized: webhook is not valid' }) | ||
|
||
return { isValidWebhook } | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { fileURLToPath } from 'node:url' | ||
import { subtle } from 'node:crypto' | ||
import { Buffer } from 'node:buffer' | ||
import { describe, it, expect } from 'vitest' | ||
import { setup, $fetch } from '@nuxt/test-utils' | ||
|
||
describe('stripe', async () => { | ||
await setup({ | ||
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), | ||
}) | ||
|
||
it('valid Stripe webhook', async () => { | ||
const timestamp = Math.floor(Date.now() / 1000) | ||
const body = 'testBody' | ||
const secretKey = 'testStripeSecretKey' | ||
const encoder = new TextEncoder() | ||
const signature = await subtle.importKey('raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) | ||
const hmac = await subtle.sign('HMAC', signature, encoder.encode(`${timestamp}.${body}`)) | ||
const computedHash = Buffer.from(hmac).toString('hex') | ||
|
||
const validSignature = `t=${timestamp},v1=${computedHash}` | ||
|
||
const headers = { 'stripe-signature': validSignature } | ||
|
||
const response = await $fetch<{ isValidWebhook: boolean }>('/api/webhooks/stripe', { | ||
method: 'POST', | ||
headers, | ||
body, | ||
}) | ||
|
||
expect(response).toStrictEqual({ isValidWebhook: true }) | ||
}) | ||
}) |