node.jstypescriptexpressstripe-paymentsnestjs

Access raw body of Stripe webhook in Nest.js


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 controller method that is needing the raw body.

function addRawBody(req, res, next) {
  req.setEncoding('utf8');

  let data = '';

  req.on('data', (chunk) => {
    data += chunk;
  });

  req.on('end', () => {
    req.rawBody = data;

    next();
  });
}

export class SubscriptionModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(addRawBody)
      .forRoutes('subscriptions/stripe');
  }
}

In the controller I am using @Req() reqand then req.rawBody to get the raw body. I need the raw body because the constructEvent of the Stripe api is using it to verify the request.

The problem is that the request is stuck. It seems that the req.on is not called either for data nor for the end event. So next() is not called in the middleware.

I did also try to use raw-body like here but I got pretty much the same result. In that case the req.readable is always false, so I am stuck there as well.

I guess this is an issue with Nest.js but I am not sure...


Solution

  • 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!

    EDIT:

    Since this question was asked, Nest.js implemented this use case out of the box. Now you can get the raw body following these steps:

    main.js

    const app = await NestFactory.create(AppModule, { rawBody: true });
    

    And then in your controller:

     @Post()
     webhook(@Req() req: RawBodyRequest<Request>) { 
      const rawBody = req.rawBody;
     }
    

    Read more here