在 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');
}
}
}
});
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' }
研究与尝试:
WEBHOOK_ID
均已正确设置。问题:
任何指导或见解将不胜感激。
最后,我解决了这个问题,因为 Paypal 非常关心它的 webhook 安全性,我无法使用 Postman 在本地运行我的 webhook,解决方案是下载并设置 ngrok 并将我的 webhook URL 更改为 URL ngrok 为我生成并在 URL 末尾添加“/paypal/subscriptions/webhook”,如下所示:
https://ngrok-url/paypal/subscriptions/webhook
关于“沙箱 Webhooks”