typescript 93 lines · 4 tabs

Verify Stripe Webhook Signatures in a NestJS Guard Before the Handler

Shared by codesnips Aug 2026
4 tabs
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // rawBody: true preserves the exact bytes Stripe signed
  const app = await NestFactory.create(AppModule, { rawBody: true });
  app.enableShutdownHooks();
  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();
4 files · typescript Explain with highlit

Webhook endpoints are public and unauthenticated by design, so the only proof that a request truly came from the provider is a cryptographic signature computed over the raw request body. This snippet moves that verification out of the controller and into a NestJS guard, so an unsigned or tampered payload is rejected before any handler logic runs. Doing the check in a guard keeps the controller focused on business logic and makes the security boundary explicit and reusable across multiple webhook routes.

The critical constraint is that signature verification must run against the exact bytes the provider signed, not a re-serialized object. In main.ts the app is created with rawBody: true, which tells NestJS to expose the untouched buffer on req.rawBody. Without this, the default JSON body parser would consume and re-encode the payload, and the HMAC would never match — a classic and frustrating pitfall.

The StripeWebhookGuard implements CanActivate and pulls the stripe-signature header plus the raw buffer off the request. Rather than hand-rolling HMAC comparison, it delegates to stripe.webhooks.constructEvent, which internally recomputes the signature, enforces a timestamp tolerance to prevent replay attacks, and throws on mismatch. The guard catches that error and raises a BadRequestException, so a bad signature returns 400 instead of leaking a stack trace. On success it stashes the verified Stripe.Event back onto the request via req.stripeEvent so the handler need not re-parse anything.

A small @StripeEvent() param decorator, defined with createParamDecorator, reads that stashed event, giving the controller a clean typed argument. In StripeWebhookController the guard is attached with @UseGuards(StripeWebhookGuard), and the handler receives an already-trusted event. Note the HttpCode(200) and the fast switch on event.type — providers retry on non-2xx responses, so acknowledging quickly and handling unknown types gracefully matters. The ConfigService supplies both the STRIPE_SECRET_KEY and the endpoint's STRIPE_WEBHOOK_SECRET, keeping secrets out of code. This pattern generalizes to any HMAC-signed webhook: verify the raw body in a guard, reject early, and pass a trusted object downstream.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
kotlin
package com.example.myapp

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp

Dependency injection with Hilt

kotlin android hilt
by Alex Chen 3 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Verify Stripe Webhook Signatures in a NestJS Guard Before the Handler — share card
Link copied