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