lukeautry / tsoa

Build OpenAPI-compliant REST APIs using TypeScript and Node
MIT License
3.48k stars 498 forks source link

Accessing raw body: Stripe webhook signing #1645

Closed kevinroleke closed 1 month ago

kevinroleke commented 3 months ago

My application needs to receive and verify webhook events from Stripe. This works by passing a Buffer of the raw POST request body into the Stripe client. Unfortunately, @Body() does not suffice.

Sorting

Expected Behavior

I should be able to do something like this

@Post('stripe-webhook')
public async stripeWebhook(@Raw() rawBody: Buffer, @Header('stripe-signature') signature: string): Promise<void> {
...
}
github-actions[bot] commented 3 months ago

Hello there kevinroleke 👋

Thank you for opening your very first issue in this project.

We will try to get back to you as soon as we can.👀

a-ledu commented 2 months ago

Hi @kevinroleke I had the same problem, we can't access the raw body right now, maybe also because it would use more memory maybe?

Here is how I did if you didn't found a workaround already :

1/ In your express init, before the body is parsed, use "verify" to save the raw body

  // to support url-encoded bodies
  app.use(json({ verify: rawBodySaver, limit: '1mb' }));
  // to support JSON-encoded bodies ("100kb" by default)
  app.use(urlencoded({ verify: rawBodySaver, extended: true }));

The function can save the raw body only for specific endpoints to avoid using unnecessary memory

import { Request, Response } from 'express';

export interface RawBodyRequest extends Request { rawBody?: string; }
export const rawBodySaver = (req:RawBodyRequest, _res:Response, buf?: Buffer, encoding?: string) => {
  if ((req.originalUrl.startsWith(`/stripe-webhook`)) && buf?.length) {
    req.rawBody = buf.toString(encoding as BufferEncoding || 'utf8');
  }
};

2/ On your endpoint, you can now use the extended request like:

  @Get('example')
  public async slackWebhook(
    @Request() request: RawBodyRequest,
  ): Promise<void> {
    // your logic with request.rawBody
  }

But the best if you have multiple endpoints with slack is to use a middleware with @Security('slack') https://tsoa-community.github.io/docs/authentication.html

  if (securityName === 'slack') {
    const reqTimestamp = request.headers['x-slack-request-timestamp'] as string;
    const slackSignature: string = request.headers['x-slack-signature'] as string;
    // Check if the request is a ssl check from slack because ssl check does not contains signature
    if (!slackSignature && isSlackSSLCheck(request)) return;
    if (!process.env.SLACK_SIGNING_SECRET) throw new Error('Missing Slack signing secret');
    const baseStr = `v0:${reqTimestamp}:${rawBody}`;
        const expectedSignature = `v0=${createHmac('sha256', process.env.SLACK_SIGNING_SECRET)
          .update(baseStr, 'utf8')
          .digest('hex')}`;
    if (expectedSignature !== slackSignature) {
      throw new Error('Invalid slack signature');
    }
  }
github-actions[bot] commented 1 month ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days