Calendly Webhook与Node.js集成问题:签名验证失败求助
Calendly Webhook 签名验证失败问题排查与修复
核心问题分析
你的签名验证逻辑存在三个关键错误:
- 错误处理签名头格式
- 使用解析后再序列化的payload而非原始请求体
- Postman测试时签名生成方式错误
修复步骤
1. 正确提取签名头中的验证值
Calendly的Calendly-Webhook-Signature头格式为t=123456789,v1=abcdef123456,你需要提取v1=后面的哈希值,而非直接将整个头字符串与生成的digest对比。
2. 使用原始请求体计算签名
Express的req.body是解析后的JSON对象,调用JSON.stringify(req.body)会改变原始JSON的键顺序或空格格式,导致与Calendly签名时使用的原始payload不一致。需要直接读取原始请求缓冲。
修改后的签名验证函数示例:
const crypto = require('crypto'); const verifyWebhookSignature = (rawBody, signatureHeader, webhookSecret) => { // 解析签名头,提取v1部分 const signatureParts = signatureHeader.split(','); const v1Signature = signatureParts.find(part => part.startsWith('v1=')).split('=')[1]; // 用原始请求体计算HMAC const hmac = crypto.createHmac('sha256', webhookSecret); const digest = hmac.update(rawBody).digest('hex'); // 对比签名(使用 timingSafeEqual 防止时序攻击) return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(v1Signature)); };
3. 修改路由以获取原始请求体
在Express中,需要配置中间件以保留原始请求体,不能直接依赖req.body。修改路由配置:
// 添加中间件获取原始body,需在body-parser之前或使用raw中间件 AdminRouter.use('/calendly-webhook', express.raw({ type: 'application/json' })); AdminRouter.post('/calendly-webhook', async (req, res) => { try { console.log('Webhook received:'); const signatureHeader = req.headers['calendly-webhook-signature']; const webhookSecret = process.env.CALENDLY_SIGNING_KEY; // 将原始buffer转为字符串,用于后续解析事件 const rawBodyStr = req.body.toString(); const event = JSON.parse(rawBodyStr); if (!verifyWebhookSignature(req.body, signatureHeader, webhookSecret)) { return res.status(401).send('Invalid signature'); } switch (event.event) { case 'invitee.created': await handleInviteeCreated(event.payload); break; case 'invitee.canceled': await handleInviteeCanceled(event.payload); break; case 'invitee_no_show.created': await handleInviteeNoShow(event.payload); break; case 'invitee_no_show.deleted': await handleInviteeNoShowDeleted(event.payload); break; default: console.log(`Unhandled event type: ${event.event}`); } res.status(200).send('Webhook received successfully'); } catch (error) { console.error('Error processing Calendly webhook:', error); res.status(500).send('Error processing webhook'); } });
4. 正确进行Postman测试
你当前的测试签名错误,正确的测试步骤:
- 准备好测试用的payload字符串(和请求体完全一致,包括空格、换行)
- 使用你的signing_key对该字符串进行HMAC-SHA256哈希,得到十六进制结果
- 构造签名头:
t=当前时间戳,v1=刚才生成的哈希值 - 将该签名头添加到Postman请求中
示例Node.js代码生成测试签名:
const crypto = require('crypto'); const payload = '{"event": "invitee.created","payload": {"event": {"start_time": "2024-09-01T10:00:00Z","end_time": "2024-09-01T10:30:00Z"},"invitee": {"email": "test@example.com","name": "Test User"}}}'; const signingKey = '5mEzn9C-I28UtwOjZJtFoob0sAAFZ95GbZkqj4y3i0I'; const hmac = crypto.createHmac('sha256', signingKey); const digest = hmac.update(payload).digest('hex'); const timestamp = Math.floor(Date.now() / 1000); console.log(`Calendly-Webhook-Signature: t=${timestamp},v1=${digest}`);
5. 确认signing_key一致性
确保process.env.CALENDLY_SIGNING_KEY的值与创建webhook订阅时提交的signing_key完全一致,包括所有字符、连字符,没有多余的空格或换行。
额外注意事项
- Calendly建议验证签名中的timestamp,防止重放攻击,你可以添加逻辑检查timestamp是否在合理的时间范围内(比如5分钟内)
- 确保你的服务器可以被Calendly公网访问,否则实际预约触发时Calendly无法发送webhook请求
内容的提问来源于stack exchange,提问作者Yohav Rn
相关产品推荐
相关产品推荐

