如何修复 PayPal Node.js webhook 中的“Webhook 签名验证失败”?

问题描述 投票:0回答:1

在 Node.js 应用程序中实现 PayPal Webhooks 时,我遇到了

401 Unauthorized
错误,并显示消息“Webhook 签名验证失败”。当我使用
POST
http://localhost:3001/paypal/subscriptions/webhook
发出
Postman
请求时,就会出现此问题。

背景:

为了模拟 webhook 事件,我一直在使用 Webhook.site 来捕获 PayPal 的 webhook 调用并提取必要的标头。然后,这些标头会手动添加到 Postman 请求中,以模仿来自 PayPal 的实际 Webhook 调用。标题包括:

  • paypal-auth-algo

  • paypal-cert-url

  • paypal-transmission-id

  • paypal-transmission-sig

  • paypal-transmission-time

尽管确保了这些标头的正确性,但 Webhook 签名的验证始终失败。

代码:


paypalRouter.post("/subscriptions/webhook", async (req, res) => {

  console.log("Received webhook event", req.body); 

  try {
   
    console.log('Headers:', {
      'paypal-auth-algo': req.headers['paypal-auth-algo'],
      'paypal-cert-url': req.headers['paypal-cert-url'],
      'paypal-transmission-id': req.headers['paypal-transmission-id'],
      'paypal-transmission-sig': req.headers['paypal-transmission-sig'],
      'paypal-transmission-time': req.headers['paypal-transmission-time'],
    });
    const webhookEvent = req.body;

    console.log("Webhook event received:", webhookEvent.event_type);


    const verification = {
      auth_algo: req.headers['paypal-auth-algo'],
      cert_url: req.headers['paypal-cert-url'],
      transmission_id: req.headers['paypal-transmission-id'],
      transmission_sig: req.headers['paypal-transmission-sig'],
      transmission_time: req.headers['paypal-transmission-time'],
      webhook_id: process.env.WEBHOOK_ID,
      webhook_event: webhookEvent,
    };
    console.log('Final Verification Request Payload:', JSON.stringify(verification, null, 2));

    console.log('Verification Payload:', verification);

    const params = new URLSearchParams();
    params.append("grant_type", "client_credentials");

    const authResponse = await axios.post(
      "https://api-m.sandbox.paypal.com/v1/oauth2/token",
      params,
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        auth: {
          username: process.env.PAYPAL_CLIENT_ID,
          password: process.env.PAYPAL_SECRET,
        },
      }
    );

    const accessToken = authResponse.data.access_token;

    

    const verifyResponse = await axios.post(
      "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature",
      verification,
      {
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${accessToken}`
        },
      }
    );

 
    if (verifyResponse.data.verification_status === "SUCCESS") {
      // Handle different event types as needed
      if (webhookEvent.event_type === "BILLING.SUBSCRIPTION.ACTIVATED") {
        // Extracting the custom_id and subscription ID from the webhook event
        const userId = webhookEvent.resource.custom_id; // Adjusted based on actual data structure
        const subscriptionId = webhookEvent.resource.id; // Adjusted based on actual data structure

        console.log(`Attempting to confirm subscription for user ${userId} with subscription ID ${subscriptionId}`);

        try {
          // Updating the user's subscription status to 'confirmed'
          const updatedUser = await User.findOneAndUpdate(
              { _id: userId, subscriptionId: subscriptionId }, 
              { $set: { subscriptionStatus: 'confirmed' }},
              { new: true }
          );

          if (updatedUser) {
              console.log("Subscription confirmed for user:", userId);
          } else {
              console.log("No matching user document to update or subscription ID mismatch.");
          }

          return res.status(200).send('Subscription confirmed');
        } catch (error) {
          console.error("Error confirming subscription:", error);
          return res.status(500).send("Error updating subscription status.");
        }
      }
    } else {
      console.log("Failed to verify webhook signature:", verifyResponse.data);
      return res.status(401).send('Webhook signature verification failed');
    }
  }
  }
});

控制台.logs

Received webhook event: {}

Headers: {
  'paypal-auth-algo': 'SHA...',
  'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs/CERT...',
  'paypal-transmission-id': 'a5d...',
  'paypal-transmission-sig': 'Rrwi...',
  'paypal-transmission-time': '2024....'
}
Webhook event received: undefined
Final Verification Request Payload: {
  "auth_algo": "SHA-....",
  "cert_url": "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-...",
  "transmission_id": "a5d....",
  "transmission_sig": "Rrw...",
  "transmission_time": "2024....",
  "webhook_id": "string",
  "webhook_event": {}
}
Failed to verify webhook signature: { verification_status: 'FAILURE' }

研究与尝试:

  • 我已经验证标头模仿了 PayPal 为真实 Webhook 事件发送的内容。
  • 我已确保 PayPal 客户端 ID 和密码的环境变量以及我的
    WEBHOOK_ID
    均已正确设置。
  • 搜索了 Stack Overflow (SO) 并发现了类似的问题,但这些解决方案(例如,检查环境 URL、确保标头正确)并没有解决我的问题。

问题:

  1. PayPal webhook 验证是否有任何我可能错过的常见陷阱或特定配置?
  2. 问题是否源于我在 Postman 中设置标头的方式或对 PayPal webhook 验证过程的误解?

任何指导或见解将不胜感激。

node.js paypal-rest-sdk paypal-subscriptions paypal-webhooks
1个回答
0
投票

最后,我解决了这个问题,因为 Paypal 非常关心它的 webhook 安全性,我无法使用 Postman 在本地运行我的 webhook,解决方案是下载并设置 ngrok 并将我的 webhook URL 更改为 URL ngrok 为我生成并在 URL 末尾添加“/paypal/subscriptions/webhook”,如下所示:

https://ngrok-url/paypal/subscriptions/webhook

关于“沙箱 Webhooks”

© www.soinside.com 2019 - 2024. All rights reserved.