Add WebHook validation for Stripe in NestJS
background
Nest is a framework for building efficient, scalable NodeJS server-side applications. It uses progressive JavaScript with built-in and full support for TypeScript, but still allows developers to write code in pure JavaScript. It combines elements of OOP (object-oriented programming), FP (functional programming), and FRP (functional response programming).
Stripe is an American financial services and software-as-a-service company headquartered in San Francisco, California. It provides payment processing software and application programming interfaces for e-commerce sites and mobile applications. On August 4, 2020, “Suzhou High-tech Zone · 2020 Hurun Global Unicorn List” was released, and Stripe ranked 5th.
Note: The following content requires experience with NodeJS and NestJS, and additional learning if not.
Code implementation
1. Remove the built-in Http Body Parser.
Since Nest internally converts all requests directly to JavaScript objects by default, this is convenient in general, but can be problematic if we want to customize the content of the response, so we need to replace it with a custom one first.
First, passing an argument to start the application from the root disables the Parser that comes with it.
import {NestFactory} from '@nestjs/core';
import {ExpressAdapter, NestExpressApplication} from '@nestjs/platform-express';
/ / application root
import {AppModule} from '@app/app/app.module';
/ / disable bodyParser
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
{cors: true.bodyParser: false});Copy the code
2. Parser middleware
Then define three different middleware:
- Parser for Stripe
// raw-body.middleware.ts
import {Injectable, NestMiddleware} from '@nestjs/common';
import {Request, Response} from 'express';
import * as bodyParser from 'body-parser';
@Injectable(a)export class RawBodyMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => any) {
bodyParser.raw({type: '* / *'})(req, res, next); }}Copy the code
// raw-body-parser.middleware.ts
import {Injectable, NestMiddleware} from '@nestjs/common';
import {Request, Response} from 'express';
@Injectable(a)export class RawBodyParserMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => any) {
req['rawBody'] = req.body;
req.body = JSON.parse(req.body.toString()); next(); }}Copy the code
- A normal Parser for something else
// json-body.middleware.ts
import {Request, Response} from 'express';
import * as bodyParser from 'body-parser';
import {Injectable, NestMiddleware} from '@nestjs/common';
@Injectable(a)export class JsonBodyMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => any){ bodyParser.json()(req, res, next); }}Copy the code
Based on the two different scenarios above, inject it in the root App:
import {Module, NestModule, MiddlewareConsumer} from '@nestjs/common';
import {JsonBodyMiddleware} from '@app/core/middlewares/json-body.middleware';
import {RawBodyMiddleware} from '@app/core/middlewares/raw-body.middleware';
import {RawBodyParserMiddleware} from '@app/core/middlewares/raw-body-parser.middleware';
import {StripeController} from '@app/events/stripe/stripe.controller';
@Module(a)export class AppModule implements NestModule {
public configure(consumer: MiddlewareConsumer): void {
consumer
.apply(RawBodyMiddleware, RawBodyParserMiddleware)
.forRoutes(StripeController)
.apply(JsonBodyMiddleware)
.forRoutes(The '*'); }}Copy the code
RawBodyMiddleware and RawBodyParserMiddleware are applied to the Controller that actually processes webhooks, which will add an unconverted key to the original transformation result. Return the Raw Response also to the program for further processing; For the rest, use a default Json Parser that has the same effect as the built-in one.
3. Interceptor validator
Next, we write an Interceptor that is used for verification. This Interceptor handles the verification, passing if it is normal, and intercepting it if it is not.
import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import Stripe from 'stripe';
import {Observable} from 'rxjs';
import {ConfigService} from '@app/shared/config/config.service';
import {StripeService} from '@app/shared/services/stripe.service';
@Injectable(a)export class StripeInterceptor implements NestInterceptor {
private readonly stripe: Stripe;
private readonly logger = new Logger(StripeInterceptor.name);
constructor(
private readonly configService: ConfigService,
private readonly stripeService: StripeService,
) {
/ / is equivalent to
// this.stripe = new Stripe(secret, {} as Stripe.StripeConfig);
this.stripe = stripeService.getClient();
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const signature = request.headers['stripe-signature'];
// For Stripe authentication, different webhooks have different keys
// In this case, you only need to add the corresponding key according to the service requirements
const CHARGE_SUCCEEDED = this.configService.get(
'STRIPE_SECRET_CHARGE_SUCCEEDED',);const secrets = {
'charge.succeed': CHARGE_SUCCEEDED,
};
const secret = secrets[request.body['type']].if(! secret) {throw new BadRequestException({
status: 'Oops, Nice Try'.message: 'WebHook Error: Function not supported'}); }try {
this.logger.log(signature, 'Stripe WebHook Signature');
this.logger.log(request.body, 'Stripe WebHook Body');
const event = this.stripe.webhooks.constructEvent(
request.rawBody,
signature,
secret,
);
this.logger.log(event, 'Stripe WebHook Event');
} catch (e) {
this.logger.error(e.message, 'Stripe WebHook Validation');
throw new BadRequestException({
status: 'Oops, Nice Try'.message: `WebHook Error: ${e.message as string}`}); }returnnext.handle(); }}Copy the code
4. Apply to Controller
Finally, we add this Interceptor to our WebHook Controller.
import {Controller, UseInterceptors} from '@nestjs/common';
import {StripeInterceptor} from '@app/core/interceptors/stripe.interceptor';
@Controller('stripe')
@UseInterceptors(StripeInterceptor)
export class StripeController {}
Copy the code
And we’re done!