Access raw body of Stripe webhook in Nest.js

后端 未结 4 1682
抹茶落季
抹茶落季 2021-02-03 11:36

I need to access the raw body of the webhook request from Stripe in my Nest.js application.

Following this example, I added the below to the module which has a controlle

相关标签:
4条回答
  • 2021-02-03 12:14

    This is my take on getting the raw(text)body in NestJS's hander:

    1. configure the app with preserveRawBodyInRequest as shown in JSDoc example (to restrict only for stripe webhook use "stripe-signature" as filter header)
    2. use RawBody decorator in handler to retrieve the raw(text)body

    raw-request.decorator.ts:

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { NestExpressApplication } from "@nestjs/platform-express";
    
    import { json, urlencoded } from "express";
    import type { Request } from "express";
    import type http from "http";
    
    export const HTTP_REQUEST_RAW_BODY = "rawBody";
    
    /**
     * make sure you configure the nest app with <code>preserveRawBodyInRequest</code>
     * @example
     * webhook(@RawBody() rawBody: string): Record<string, unknown> {
     *   return { received: true };
     * }
     * @see preserveRawBodyInRequest
     */
    export const RawBody = createParamDecorator(
      async (data: unknown, context: ExecutionContext) => {
        const request = context
          .switchToHttp()
          .getRequest<Request>()
        ;
    
        if (!(HTTP_REQUEST_RAW_BODY in request)) {
          throw new Error(
            `RawBody not preserved for request in handler: ${context.getClass().name}::${context.getHandler().name}`,
          );
        }
    
        const rawBody = request[HTTP_REQUEST_RAW_BODY];
    
        return rawBody;
      },
    );
    
    /**
     * @example
     * const app = await NestFactory.create<NestExpressApplication>(
     *   AppModule,
     *   {
     *     bodyParser: false, // it is prerequisite to disable nest's default body parser
     *   },
     * );
     * preserveRawBodyInRequest(
     *   app,
     *   "signature-header",
     * );
     * @param app
     * @param ifRequestContainsHeader
     */
    export function preserveRawBodyInRequest(
      app: NestExpressApplication,
      ...ifRequestContainsHeader: string[]
    ): void {
      const rawBodyBuffer = (
        req: http.IncomingMessage,
        res: http.ServerResponse,
        buf: Buffer,
      ): void => {
        if (
          buf?.length
          && (ifRequestContainsHeader.length === 0
            || ifRequestContainsHeader.some(filterHeader => req.headers[filterHeader])
          )
        ) {
          req[HTTP_REQUEST_RAW_BODY] = buf.toString("utf8");
        }
      };
    
      app.use(
        urlencoded(
          {
            verify: rawBodyBuffer,
            extended: true,
          },
        ),
      );
      app.use(
        json(
          {
            verify: rawBodyBuffer,
          },
        ),
      );
    }
    
    0 讨论(0)
  • 2021-02-03 12:18

    I ran into a similar problem last night trying to authenticate a Slack token.

    The solution we wound up using did require disabling the bodyParser from the core Nest App then re-enabling it after adding a new rawBody key to the request with the raw request body.

        const app = await NestFactory.create(AppModule, {
            bodyParser: false
        });
    
        const rawBodyBuffer = (req, res, buf, encoding) => {
            if (buf && buf.length) {
                req.rawBody = buf.toString(encoding || 'utf8');
            }
        };
    
        app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true }));
        app.use(bodyParser.json({ verify: rawBodyBuffer }));
    
    

    Then in my middleware I could access it like so:

    const isVerified = (req) => {
        const signature = req.headers['x-slack-signature'];
        const timestamp = req.headers['x-slack-request-timestamp'];
        const hmac = crypto.createHmac('sha256', 'somekey');
        const [version, hash] = signature.split('=');
    
        // Check if the timestamp is too old
        // tslint:disable-next-line:no-bitwise
        const fiveMinutesAgo = ~~(Date.now() / 1000) - (60 * 5);
        if (timestamp < fiveMinutesAgo) { return false; }
    
        hmac.update(`${version}:${timestamp}:${req.rawBody}`);
    
        // check that the request signature matches expected value
        return timingSafeCompare(hmac.digest('hex'), hash);
    };
    
    export async function slackTokenAuthentication(req, res, next) {
        if (!isVerified(req)) {
            next(new HttpException('Not Authorized Slack', HttpStatus.FORBIDDEN));
        }
        next();
    }
    

    Shine On!

    0 讨论(0)
  • 2021-02-03 12:18

    For anyone looking for a more elegant solution, turn off the bodyParser in main.ts. Create two middleware functions, one for rawbody and the other for json-parsed-body.

    json-body.middleware.ts

    import { Request, Response } from 'express';
    import * as bodyParser from 'body-parser';
    import { Injectable, NestMiddleware } from '@nestjs/common';
    
    @Injectable()
    export class JsonBodyMiddleware implements NestMiddleware {
        use(req: Request, res: Response, next: () => any) {
            bodyParser.json()(req, res, next);
        }
    }
    

    raw-body.middleware.ts

    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response } from 'express';
    import * as bodyParser from 'body-parser';
    
    @Injectable()
    export class RawBodyMiddleware implements NestMiddleware {
        use(req: Request, res: Response, next: () => any) {
            bodyParser.raw({type: '*/*'})(req, res, next);
        }
    }
    

    And apply the middleware functions to appropriate routes in app.module.ts.

    app.module.ts

    [...]
    
    export class AppModule implements NestModule {
        public configure(consumer: MiddlewareConsumer): void {
            consumer
                .apply(RawBodyMiddleware)
                .forRoutes({
                    path: '/stripe-webhooks',
                    method: RequestMethod.POST,
                })
                .apply(JsonBodyMiddleware)
                .forRoutes('*');
        }
    }
    
    [...]
    

    BTW req.rawbody has been removed from express long ago.

    https://github.com/expressjs/express/issues/897

    0 讨论(0)
  • 2021-02-03 12:24

    Today,

    as I am using NestJS and Stripe

    I installed body-parser (npm), then in the main.ts, just add

     app.use('/payment/hooks', bodyParser.raw({type: 'application/json'}));
    

    and it will be restricted to this route ! no overload

    0 讨论(0)
提交回复
热议问题