Ver Fonte

fix(payments-plugin): Fix Stripe controller crashing server instance (#2454)

Fixes #2450

Co-authored-by: Balázs Gallay <balazs.gallay@zooshgroup.com>
Balazs Gallay há 2 anos atrás
pai
commit
b0ece212dc

+ 97 - 2
packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts

@@ -10,7 +10,9 @@ import { CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS } from '@vendure/core/e2e/graph
 import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
 import gql from 'graphql-tag';
 import nock from 'nock';
+import fetch from 'node-fetch';
 import path from 'path';
+import { Stripe } from 'stripe';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
@@ -293,6 +295,101 @@ describe('Stripe payments', () => {
         });
     });
 
+    // https://github.com/vendure-ecommerce/vendure/issues/2450
+    it('Should not crash on signature validation failure', async () => {
+        const MOCKED_WEBHOOK_PAYLOAD = {
+            id: 'evt_0',
+            object: 'event',
+            api_version: '2022-11-15',
+            data: {
+                object: {
+                    id: 'pi_0',
+                    currency: 'usd',
+                    status: 'succeeded',
+                },
+            },
+            livemode: false,
+            pending_webhooks: 1,
+            request: {
+                id: 'req_0',
+                idempotency_key: '00000000-0000-0000-0000-000000000000',
+            },
+            type: 'payment_intent.succeeded',
+        };
+
+        const payloadString = JSON.stringify(MOCKED_WEBHOOK_PAYLOAD, null, 2);
+
+        const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
+            method: 'post',
+            body: payloadString,
+            headers: { 'Content-Type': 'application/json' },
+        });
+
+        // We didn't provided any signatures, it should result in a 400 - Bad request
+        expect(result.status).toEqual(400);
+    });
+
+    // TODO: Contribution welcome: test webhook handling and order settlement
+    // https://github.com/vendure-ecommerce/vendure/issues/2450
+    it("Should validate the webhook's signature properly", async () => {
+        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+
+        const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+        order = activeOrder!;
+
+        const MOCKED_WEBHOOK_PAYLOAD = {
+            id: 'evt_0',
+            object: 'event',
+            api_version: '2022-11-15',
+            data: {
+                object: {
+                    id: 'pi_0',
+                    currency: 'usd',
+                    metadata: {
+                        orderCode: order.code,
+                        orderId: parseInt(order.id.replace('T_', ''), 10),
+                        channelToken: E2E_DEFAULT_CHANNEL_TOKEN,
+                    },
+                    amount_received: order.totalWithTax,
+                    status: 'succeeded',
+                },
+            },
+            livemode: false,
+            pending_webhooks: 1,
+            request: {
+                id: 'req_0',
+                idempotency_key: '00000000-0000-0000-0000-000000000000',
+            },
+            type: 'payment_intent.succeeded',
+        };
+
+        const payloadString = JSON.stringify(MOCKED_WEBHOOK_PAYLOAD, null, 2);
+        const stripeWebhooks = new Stripe('test-api-secret', { apiVersion: '2023-08-16' }).webhooks;
+        const header = stripeWebhooks.generateTestHeaderString({
+            payload: payloadString,
+            secret: 'test-signing-secret',
+        });
+
+        const event = stripeWebhooks.constructEvent(payloadString, header, 'test-signing-secret');
+        expect(event.id).to.equal(MOCKED_WEBHOOK_PAYLOAD.id);
+        await setShipping(shopClient);
+        // Due to the `this.orderService.transitionToState(...)` fails with the internal lookup by id,
+        // we need to put the order into `ArrangingPayment` state manually before calling the webhook handler.
+        // const transitionResult = await adminClient.query(TRANSITION_TO_ARRANGING_PAYMENT, { id: order.id });
+        // expect(transitionResult.transitionOrderToState.__typename).toBe('Order')
+
+        const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
+            method: 'post',
+            body: payloadString,
+            headers: { 'Content-Type': 'application/json', 'Stripe-Signature': header },
+        });
+
+        // I would expect to the status to be 200, but at the moment either the
+        // `orderService.transitionToState()` or the `orderService.addPaymentToOrder()`
+        // throws an error of 'error.entity-with-id-not-found'
+        expect(result.status).toEqual(200);
+    });
+
     // https://github.com/vendure-ecommerce/vendure/issues/1630
     describe('currencies with no fractional units', () => {
         let japanProductId: string;
@@ -401,6 +498,4 @@ describe('Stripe payments', () => {
             expect(createPaymentIntentPayload.currency).toBe('jpy');
         });
     });
-
-    // TODO: Contribution welcome: test webhook handling and order settlement
 });

+ 1 - 0
packages/payments-plugin/src/stripe/raw-body.middleware.ts

@@ -8,6 +8,7 @@ import { RequestWithRawBody } from './types';
  * Stripe to properly verify webhook events.
  */
 export const rawBodyMiddleware = raw({
+    type: '*/*',
     verify(req: RequestWithRawBody, res: http.ServerResponse, buf: Buffer, encoding: string) {
         if (Buffer.isBuffer(buf)) {
             req.rawBody = Buffer.from(buf);

+ 12 - 3
packages/payments-plugin/src/stripe/stripe.controller.ts

@@ -45,7 +45,7 @@ export class StripeController {
             return;
         }
 
-        const event = request.body as Stripe.Event;
+        const event = JSON.parse(request.body.toString()) as Stripe.Event;
         const paymentIntent = event.data.object as Stripe.PaymentIntent;
 
         if (!paymentIntent) {
@@ -120,11 +120,20 @@ export class StripeController {
                     `Error adding payment to order ${orderCode}: ${addPaymentToOrderResult.message}`,
                     loggerCtx,
                 );
+                return;
             }
+
+            // The payment intent ID is added to the order only if we can reach this point.
+            Logger.info(
+                `Stripe payment intent id ${paymentIntent.id} added to order ${orderCode}`,
+                loggerCtx,
+            );
         });
 
-        Logger.info(`Stripe payment intent id ${paymentIntent.id} added to order ${orderCode}`, loggerCtx);
-        response.status(HttpStatus.OK).send('Ok');
+        // Send the response status only if we didn't sent anything yet.
+        if (!response.headersSent) {
+            response.status(HttpStatus.OK).send('Ok');
+        }
     }
 
     private async createContext(channelToken: string, req: RequestWithRawBody): Promise<RequestContext> {