You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Calendly Webhook与Node.js集成问题:签名验证失败求助

Calendly Webhook 签名验证失败问题排查与修复

核心问题分析

你的签名验证逻辑存在三个关键错误:

  1. 错误处理签名头格式
  2. 使用解析后再序列化的payload而非原始请求体
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 23:58:22