Access raw body of Stripe webhook in Nest.js

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


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);
    }
}

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('*');
    }
}

[...]

And tweak initialization of Nest to turn off bodyParser:

main.ts

[...]

const app = await NestFactory.create(AppModule, { bodyParser: false })

[...]

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

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