Skip to content
Merged
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
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,13 @@ Go to [playground/.env.example](./playground/.env.example) or [playground/nuxt.c

You can add your favorite webhook validator by creating a new file in [src/runtime/server/lib/validators/](./src/runtime/server/lib/validators/)

## Example
## Examples

### Basic

Validate a GitHub webhook in a server API route.

`~/server/api/webhooks/github.post.ts`
`~~/server/api/webhooks/github.post.ts`

```js
export default defineEventHandler(async (event) => {
Expand All @@ -122,6 +124,25 @@ export default defineEventHandler(async (event) => {
})
```

## Reading the request body

Make sure to read the body after validating the webhook, as most validators rely on a caching/cloning of the raw body to compute the signature and validate the webhook.

```js
export default defineEventHandler(async (event) => {
const isValidWebhook = await isValidGitHubWebhook(event)

if (!isValidWebhook) {
throw createError({ status: 401, message: 'Unauthorized: webhook is not valid' })
}

// Make sure to read the body after validating the webhook
const body = await readBody(event)

return { isValidWebhook }
})
```

# Development

```sh
Expand Down
4 changes: 2 additions & 2 deletions playground/server/api/webhooks/slack.post.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export default defineEventHandler(async (event) => {
const isValidWebhook = await isValidSlackWebhook(event)

const body = await readBody(event)

if (body.challenge) {
return body.challenge
}

const isValidWebhook = await isValidSlackWebhook(event)

if (!isValidWebhook) throw createError({ status: 401, message: 'Unauthorized: webhook is not valid' })

return { isValidWebhook }
Expand Down
16 changes: 15 additions & 1 deletion src/runtime/server/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { subtle, type webcrypto } from 'node:crypto'
import { Buffer } from 'node:buffer'
import { snakeCase } from 'scule'
import { type H3Event, createError } from 'h3'
import { type H3Event, createError, readRawBody } from 'h3'
import type { RuntimeConfig } from '@nuxt/schema'
import { useRuntimeConfig } from '#imports'

Expand Down Expand Up @@ -90,3 +90,17 @@ export const validateSha256 = async (
) => {
return hash === await sha256(payload, options?.encoding)
}

export const readRawBodyClone = async (event: H3Event): Promise<string | undefined> => {
const hasClone = 'clone' in (event.req ?? {}) // prepare h3 v2 support
const body = hasClone ? await (event.req as unknown as Request).clone().text() : await readRawBody(event)
if (!hasClone) {
(event as { _requestBody?: string })._requestBody = body
}
return body
}

export const readBodyClone = async <T = unknown>(event: H3Event): Promise<T | undefined> => {
const rawBody = await readRawBodyClone(event)
return rawBody ? JSON.parse(rawBody) as T : undefined
}
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/discord.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { verifyPublicSignature, ED25519, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { verifyPublicSignature, ED25519, ensureConfiguration, readRawBodyClone } from '../helpers'

const DISCORD_SIGNATURE = 'x-signature-ed25519'
const DISCORD_SIGNATURE_TIMESTAMP = 'x-signature-timestamp'
Expand All @@ -14,7 +14,7 @@ export const isValidDiscordWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('discord', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookSignature = headers[DISCORD_SIGNATURE]
const webhookTimestamp = headers[DISCORD_SIGNATURE_TIMESTAMP]
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/dropbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const DROPBOX_SIGNATURE = 'X-Dropbox-Signature'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidDropboxWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('dropbox', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookSignature = headers[DROPBOX_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/fourthwall.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const FOURTHWALL_SIGNATURE = 'X-Fourthwall-Hmac-SHA256'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidFourthwallWebhook = async (event: H3Event): Promise<boolean>
const config = ensureConfiguration('fourthwall', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookSignature = headers[FOURTHWALL_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const GITHUB_SIGNATURE = 'X-Hub-Signature-256'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidGitHubWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('github', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const header = headers[GITHUB_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/gitlab.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { ensureConfiguration, readRawBodyClone } from '../helpers'

const GITLAB_TOKEN = 'X-Gitlab-Token'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidGitLabWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('gitlab', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const header = headers[GITLAB_TOKEN]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/heroku.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const HEROKU_HMAC = 'Heroku-Webhook-Hmac-SHA256'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidHerokuWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('heroku', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const header = headers[HEROKU_HMAC]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/hygraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const DEFAULT_TOLERANCE = 300 // 5 minutes tolerance
const HYGRAPH_SIGNATURE = 'gcms-signature'
Expand Down Expand Up @@ -38,7 +38,7 @@ export const isValidHygraphWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('hygraph', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const hygraphSignature = headers[HYGRAPH_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/kick.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { verifyPublicSignature, RSASSA_PKCS1_v1_5_SHA256, stripPemHeaders } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { verifyPublicSignature, RSASSA_PKCS1_v1_5_SHA256, stripPemHeaders, readRawBodyClone } from '../helpers'
import { useRuntimeConfig } from '#imports'

const KICK_MESSAGE_ID = 'Kick-Event-Message-Id'.toLowerCase()
Expand Down Expand Up @@ -27,7 +27,7 @@ export const isValidKickWebhook = async (event: H3Event): Promise<boolean> => {
const config = useRuntimeConfig(event).webhook.kick

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const messageId = headers[KICK_MESSAGE_ID]
const messageTimestamp = headers[KICK_MESSAGE_TIMESTAMP]
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/mailchannels.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { verifyPublicSignature, ED25519, validateSha256, stripPemHeaders } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { verifyPublicSignature, ED25519, validateSha256, stripPemHeaders, readRawBodyClone } from '../helpers'
import { useRuntimeConfig } from '#imports'

const MAILCHANNELS_CONTENT_DIGEST = 'content-digest'
Expand Down Expand Up @@ -50,7 +50,7 @@ export const isValidMailChannelsWebhook = async (event: H3Event): Promise<boolea
const config = useRuntimeConfig(event).webhook.mailchannels

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const contentDigest = headers[MAILCHANNELS_CONTENT_DIGEST]
const messageSignature = headers[MAILCHANNELS_SIGNATURE]
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/meta.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const META_SIGNATURE = 'X-Hub-Signature-256'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidMetaWebhook = async (event: H3Event): Promise<boolean> => {
const config = ensureConfiguration('meta', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const signatureHeader = headers[META_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/nuxthub.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { ensureConfiguration, sha256 } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { ensureConfiguration, readRawBodyClone, sha256 } from '../helpers'

const NUXTHUB_SIGNATURE = 'x-nuxthub-signature'

Expand All @@ -13,7 +13,7 @@ export const isValidNuxtHubWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('nuxthub', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookSignature = headers[NUXTHUB_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/paddle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const MAX_VALID_TIME_DIFFERENCE = 5
const PADDLE_SIGNATURE = 'paddle-signature'
Expand Down Expand Up @@ -29,7 +29,7 @@ export const isValidPaddleWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('paddle', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const paddleSignature = headers[PADDLE_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/paypal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readBody } from 'h3'
import { ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { ensureConfiguration, readBodyClone } from '../helpers'

const baseAPI = import.meta.dev ? 'https://api-m.sandbox.paypal.com/v1' : 'https://api-m.paypal.com/v1'

Expand All @@ -13,7 +13,7 @@ export const isValidPaypalWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('paypal', event)

const headers = getRequestHeaders(event)
const body = await readBody(event)
const body = await readBodyClone(event)

if (!body || !headers) return false

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/polar.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const POLAR_SIGNATURE_ID = 'webhook-id'
const POLAR_SIGNATURE = 'webhook-signature'
Expand All @@ -16,7 +16,7 @@ export const isValidPolarWebhook = async (event: H3Event): Promise<boolean> => {
const config = ensureConfiguration('polar', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookId = headers[POLAR_SIGNATURE_ID]
const webhookSignature = headers[POLAR_SIGNATURE]
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/shopify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const SHOPIFY_SIGNATURE = 'X-Shopify-Hmac-Sha256'.toLowerCase()

Expand All @@ -13,7 +13,7 @@ export const isValidShopifyWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('shopify', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookSignature = headers[SHOPIFY_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/slack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const SLACK_SIGNATURE = 'X-Slack-Signature'.toLowerCase()
const SLACK_TIMESTAMP = 'X-Slack-Request-Timestamp'.toLowerCase()
Expand All @@ -15,7 +15,7 @@ export const isValidSlackWebhook = async (event: H3Event): Promise<boolean> => {
const config = ensureConfiguration('slack', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const fullSignature = headers[SLACK_SIGNATURE]
const timestamp = headers[SLACK_TIMESTAMP]
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const DEFAULT_TOLERANCE = 300
const STRIPE_SIGNATURE = 'Stripe-Signature'.toLowerCase()
Expand Down Expand Up @@ -29,7 +29,7 @@ export const isValidStripeWebhook = async (event: H3Event): Promise<boolean> =>
const config = ensureConfiguration('stripe', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const stripeSignature = headers[STRIPE_SIGNATURE]

Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/lib/validators/svix.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type H3Event, getRequestHeaders, readRawBody } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration } from '../helpers'
import { type H3Event, getRequestHeaders } from 'h3'
import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../helpers'

const SVIX_SIGNATURE_ID = 'svix-id'
const SVIX_SIGNATURE = 'svix-signature'
Expand All @@ -15,7 +15,7 @@ export const isValidSvixWebhook = async (event: H3Event): Promise<boolean> => {
const config = ensureConfiguration('svix', event)

const headers = getRequestHeaders(event)
const body = await readRawBody(event)
const body = await readRawBodyClone(event)

const webhookId = headers[SVIX_SIGNATURE_ID]
const webhookSignature = headers[SVIX_SIGNATURE]
Expand Down
Loading