9
votes

Accéder au corps brut du webhook Stripe dans Nest.js

Je dois accéder au corps brut de la demande de webhook de Stripe dans mon application Nest.js.

À la suite de cet exemple, j'ai ajouté ce qui suit au module qui a une méthode de contrôleur qui nécessite le corps brut.

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

Dans le contrôleur, j'utilise @Req() req , puis req.rawBody pour obtenir le corps brut. J'ai besoin du corps brut car le constructEvent de l'API Stripe l'utilise pour vérifier la demande.

Le problème est que la demande est bloquée. Il semble que le req.on ne soit appelé ni pour les données ni pour l'événement de fin. Donc next() n'est pas appelé dans le middleware.

J'ai aussi essayé d'utiliser raw-body comme ici, mais j'ai obtenu à peu près le même résultat. Dans ce cas, le req.readable est toujours faux, donc je suis coincé là aussi.

Je suppose que c'est un problème avec Nest.js mais je ne suis pas sûr ...


1 commentaires

vous n'avez probablement pas désactivé le bodyParser par défaut de bodyParser lors de la création de l'application NestApplication dans la méthode bootsrap


4 Réponses :


15
votes

J'ai rencontré un problème similaire hier soir en essayant d'authentifier un jeton Slack.

La solution que nous avons finalement utilisée nécessitait de désactiver le bodyParser de l'application Nest principale, puis de le réactiver après avoir ajouté une nouvelle clé rawBody à la requête avec le corps de la requête brute.

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

Ensuite, dans mon middleware, je pourrais y accéder comme suit:

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

Briller sur!


0 commentaires

27
votes

Pour tous ceux qui recherchent une solution plus élégante, désactivez bodyParser dans main.ts Créez deux fonctions middleware, une pour rawbody et l'autre pour json-parsed-body .

json-body.middleware.ts

[...]

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

[...]

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

Et appliquez les fonctions middleware aux routes appropriées dans app.module.ts .

app.module.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);
    }
}

BTW req.rawbody a été supprimé d' express depuis longtemps.

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


5 commentaires

C'est la meilleure solution utilisant Nest IMHO, devrait être la réponse acceptée.


C'est la meilleure solution. Merci


C'est une excellente réponse, mais rappelez-vous de modifier l'initialisation de nestjs pour désactiver bodyParser: const app = await NestFactory.create (AppModule, {bodyParser: false,})


Incroyable! Fonctionne comme un charme une fois que vous éteignez le bodyParser intégré. Pourrait-il être ajouté à la réponse?


c'est la solution optimale.



9
votes

Aujourd'hui,

car j'utilise NestJS et Stripe

J'ai installé body-parser (npm), puis dans le main.ts, il suffit d'ajouter

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

et il sera limité à cet itinéraire! pas de surcharge


1 commentaires

Dans le contrôleur de hooks, ce serait quelque chose comme ce handleWebhook(@Body() raw: Buffer)



0
votes

Voici mon point de vue sur l'obtention du corps brut (texte) dans le hander de NestJS:

  1. configurez l'application avec preserveRawBodyInRequest comme indiqué dans l'exemple JSDoc (pour restreindre uniquement le webhook stripe, utilisez "stripe-signature" comme en-tête de filtre)
  2. utiliser le décorateur RawBody dans le gestionnaire pour récupérer le corps brut (texte)

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 commentaires