Express中Twitch EventSub webhook回调请求未触发验签中间件
Twitch Eventsub Webhook 签名校验中间件不触发问题排查
问题描述
- 依据Twitch官方Eventsub文档实现订阅创建逻辑,可正常收到Twitch订阅申请接口的返回响应,但回调请求到达
/notification路由时,预期运行的签名校验中间件完全未执行,流程直接中断 - 已排查路由注册顺序,未定位到问题根因
现有实现代码
app.post('/createWebhook/:broadcasterId', (req, res) => { const createWebHookParams = { host: "api.twitch.tv", path: "helix/eventsub/subscriptions", method: 'POST', headers: { "Content-Type": "application/json", "Client-ID": clientId, "Authorization": "Bearer " + authToken } } const createWebHookBody = { "type": "channel.follow", "version": "1", "condition": { "broadcaster_user_id": req.params.broadcasterId }, "transport": { "method": "webhook", "callback": "ngrokURL/notification", "secret": webhookSecret } } let responseData = "" const webhookReq = https.request(createWebHookParams, (result) => { result.setEncoding('utf8') result.on('data', (d) => { responseData = responseData + d }) .on('end', (result) => { const responseBody = JSON.parse(responseData) console.log(responseBody) res.send(responseBody) }) }) webhookReq.on('error', (e) => { console.log("Error") }) webhookReq.write(JSON.stringify(createWebHookBody)) webhookReq.end() }); // 签名校验中间件未被触发 app.use(express.json({ verify: verifyTwitchSignature })); // 接收Twitch通知的POST路由 app.post('/notification', (req, res) => { console.log("incoming notificatin", req.body) res.status(200).end(); }) // 签名校验逻辑实现 const crypto = require("crypto"); const twitchSigningSecret = process.env.SECRET; const verifyTwitchSignature = (req, res, buf, encoding)=>{ const messageId = req.header("Twitch-Eventsub-Message-Id"); const timeStamp = req.header("Twitch-Eventsub-Message-Timestamp") const messageSignature = req.header("Twitch-Eventsub-Message-Signature") console.log(`Message ${messageId} Signature: `,messageSignature) if (!twitchSigningSecret){ console.log(`Twitch signing secret is empty`); throw new Error ("Twitch signing secret is empty."); } const computedSignature = "sha256=" + crypto.createHmac("sha256", twitchSigningSecret).update(messageId + timeStamp + buf).digist("hex"); console.log(`Message ${messageId} Computed Signature: `, computedSignature) if (messageSignature !== computedSignature) { throw new Error("Invalid Signature."); }else { console.log("Verification Successful"); } } module.exports = verifyTwitchSignature
具体问题与修复方案
- 变量声明时序错误,导致中间件挂载失败
- 当前代码中,
app.use(express.json(...))的执行位置早于verifyTwitchSignature的常量定义 - 由于
verifyTwitchSignature通过const声明,存在暂时性死区,执行到app.use代码行时会直接抛出ReferenceError: Cannot access 'verifyTwitchSignature' before initialization,整个express.json中间件未被成功挂载到Express实例上,自然不会触发校验逻辑 - 修复:将所有依赖导入、自定义常量、自定义中间件函数的定义,全部移动到app.use、业务路由注册的代码之前
- 当前代码中,
- 方法名拼写错误,导致校验逻辑运行时崩溃
- HMAC签名计算环节,错误调用了不存在的
digist("hex")方法,正确方法名为digest("hex") - 即便解决了时序问题,执行到这行代码时会直接抛出
TypeError,被Express默认错误捕获逻辑拦截,表现为流程中断
- HMAC签名计算环节,错误调用了不存在的
- 中间件挂载位置不规范
- 当前express.json中间件被放置在两个业务路由中间,Express按照代码书写顺序从上到下匹配请求,这种写法会导致挂载位置之前的路由完全不经过JSON解析逻辑,后续维护极易出现兼容问题
- 修复:将
app.use(express.json(...))移动到所有业务路由之前,保证所有进入Express的请求都按顺序经过中间件处理
- 未处理Twitch Webhook的首次验证流程
- Twitch注册webhook时,会先向配置的callback地址发送类型为
webhook_callback_verification的请求,要求将请求体中的challenge值以text/plain格式原样返回,否则Twitch会直接判定订阅无效,不会推送后续事件 - 当前
/notification路由收到请求后直接返回200空响应,未处理验证流程,即便前面的中间件问题修复,也无法收到后续事件通知
- Twitch注册webhook时,会先向配置的callback地址发送类型为
- 回调地址为未替换的占位符
- transport配置中的callback字段为
"ngrokURL/notification"占位内容,需要替换为公网可访问的完整HTTPS地址(如ngrok生成的域名拼接/notification路径),否则Twitch的回调请求无法到达本地服务
- transport配置中的callback字段为
调整后的代码结构参考
- 先导入所有依赖模块
- 定义签名校验的自定义中间件函数
- 挂载全局express.json中间件,绑定verify配置
- 注册所有业务路由(包含创建webhook的接口、接收通知的接口)
- 在
/notification路由中优先判断消息类型,若为初始验证请求,直接返回对应的challenge值
内容的提问来源于stack exchange,提问作者John Yacoub
相关产品推荐
相关产品推荐

