Chatworkのボットが急に黙った。
昨日まで元気に動いていたのに、Webhook署名検証を入れた瞬間、全メッセージが401で弾かれるようになった。ログを見ると1件も通っていない。正直焦った。ボットが止まると業務が止まる。
「セキュリティを強化しよう」と思ったのが裏目に出た話と、そこからどう復旧したかを書く。
なぜ署名検証を入れたのか
ChatworkのWebhookは、デフォルトだとURLさえ知っていれば誰でもリクエストを投げられる。つまり、Webhook URLが漏れた瞬間にボットを踊らせ放題だ。偽メッセージを送り込んで、後続処理を暴走させることもできる。
Chatworkはこの対策として、Webhook送信時にHMAC-SHA256の署名を付けてくれる。ヘッダーに X-ChatWorkWebhookSignature という値が入る。受信側でこの署名を検証すれば、「本当にChatworkから来たリクエストか」を判定できる。
やらない理由がない。やった。全部壊れた。
ハマり1: n8nのHeader Authは使えない
最初に試したのは、n8nのWebhookノードに標準で用意されている「Header Auth」だ。ヘッダーに特定のトークンが入っているかチェックする仕組み。
n8nの設定画面で X-Webhook-Token を指定して、Chatwork側にもそのトークンを……と思ったが、Chatwork側にカスタムヘッダーを追加する設定がない。
Chatworkが送ってくるのは X-ChatWorkWebhookSignature という独自ヘッダーで、n8nのHeader Authが期待する X-Webhook-Token ではない。ヘッダー名が違うので永遠にマッチしない。全リクエストが401。
これに気づくまで30分溶けた。ログには「Unauthorized」としか出ないので、トークンの値が間違っているのか、ヘッダー名が違うのか、判断がつかなかった。
教訓: Chatworkの署名検証は、n8nの標準Auth機能では対応できない。Codeノードで自前実装が必要。
ハマり2: rawBodyがないとHMACが合わない
Header Authを諦めて、Codeノードで自前検証に切り替えた。HMAC-SHA256の計算自体は難しくない。
ところが、計算結果が合わない。何度やっても合わない。
原因は「何に対してHMACを計算するか」だった。最初はこう書いていた。
const body = JSON.stringify($input.first().json.body);
hmac.update(body);
これだとn8nがパースしたJSONオブジェクトを再度文字列化している。元のリクエストボディと微妙に違う。キーの順序、空白、エスケープ。どれか一つでも違えばハッシュは別物になる。
必要なのはrawBody——HTTPリクエストの生のボディ文字列だ。
n8nのWebhookノードには「Raw Body」を取得するオプションがある。これをオンにすると、$input.first().json.rawBody でパース前の生文字列が取れる。
Webhookノード → Settings → Raw Body → ON
この設定を入れた瞬間、HMACが一致するようになった。地味な設定だが、これがないと永遠にハッシュが合わない。
ハマり3: トークンはbase64エンコードされている
もう一つ罠がある。Chatworkの管理画面に表示されるWebhookトークンはbase64エンコード済みだ。
最初はそのまま文字列として使っていた。
// NG: 生文字列のままHMACキーにしている
const hmac = crypto.createHmac('sha256', WEBHOOK_TOKEN);
これだと当然ハッシュが合わない。正しくはbase64デコードしてからバイナリキーとして使う。
// OK: base64デコードしてからキーにする
const key = Buffer.from(WEBHOOK_TOKEN, 'base64');
const hmac = crypto.createHmac('sha256', key);
Chatworkの公式ドキュメントにちゃんと書いてある。書いてあるのだが、急いでいると読み飛ばす。
最終的なn8n実装
全部のハマりを潰した後の構成はこうなった。
Webhook受信(rawBody ON)
↓
HMAC署名検証(Codeノード)
↓
フィルター(自分の投稿を除外等)
↓
後続処理(ボット応答・ルーティング等)
Codeノードの中身。
const crypto = require('crypto');
// ヘッダーとrawBodyを取得
const headers = $input.first().json.headers;
const rawBody = $input.first().json.rawBody;
// 署名ヘッダーの存在チェック
const signature = headers['x-chatworkwebhooksignature'];
if (!signature) {
throw new Error('Missing X-ChatWorkWebhookSignature header');
}
// トークンをbase64デコードしてHMACキーにする
const WEBHOOK_TOKEN = $env.CHATWORK_WEBHOOK_TOKEN; // 環境変数から取得
const key = Buffer.from(WEBHOOK_TOKEN, 'base64');
// HMAC-SHA256でハッシュを計算
const hmac = crypto.createHmac('sha256', key);
hmac.update(rawBody);
const expected = hmac.digest('base64');
// 署名の突き合わせ
if (signature !== expected) {
throw new Error(`Invalid signature: got ${signature}, expected ${expected}`);
}
// 検証OK → 後続ノードへ
return $input.all();
ポイントを整理する。
| 項目 | 正解 | やりがちなミス |
|---|---|---|
| 検証方法 | Codeノードで自前実装 | n8nのHeader Auth(ヘッダー名が違う) |
| ボディ | rawBody(生文字列) | JSON.stringify(再構築された文字列) |
| トークン | base64デコードしてから使う | 生文字列のまま使う |
| トークンの保管 | 環境変数 | ハードコード |
フォールバック: rawBodyが取れない場合
環境によってはrawBodyが空になるケースがある。保険としてフォールバックを入れておくと安心。
hmac.update(rawBody || JSON.stringify($input.first().json.body));
ただし、JSON.stringify側にフォールバックすると署名が合わない可能性がある。あくまで「エラーで全部止まるよりマシ」程度の保険だ。本命はrawBodyを確実に取得する設定を入れること。
復旧までの時間
署名検証を入れてボットが全停止してから、原因特定と修正完了まで約1時間。内訳はこんな感じだった。
- Header Authで詰まる: 30分
- rawBody問題に気づく: 15分
- base64デコードに気づく: 10分
- テスト・動作確認: 5分
正直、30分の時点で「署名検証やめて元に戻そうか」と思った。でもWebhook URLが漏れるリスクを考えると、ここで妥協するのは違う。1時間の投資でボットの安全性が格段に上がったので、結果的にはやってよかった。
まとめ
Chatwork Webhook × n8nで署名検証を入れるときの3つの罠。
- n8nのHeader Authは使えない → Codeノードで自前実装
- rawBodyが必要 → WebhookノードでRaw Bodyをオンにする
-
トークンはbase64 →
Buffer.from(token, 'base64')でデコードしてからキーにする
どれも公式ドキュメントを丁寧に読めばわかることだが、「動いていたものに検証を足す」というシチュエーションだと焦りが先に立つ。特にrawBodyの罠はChatworkに限らず、Webhook署名検証では定番のハマりどころなので、n8nでWebhook受けている人は覚えておいて損はない。
Chatworkシリーズ
- #1 なぜ2026年にまだChatworkを使い倒しているのか
- #2 chatwork-client-gas、ぶっちゃけいるの?
- #3 ルームの参加者データだけで、組織の人間関係マップを作った
- #4 「Chatworkに確定連絡が来たら請求書を送る」をGASで自動化する
- #5 Chatwork MCPを繋いだら、17ルームの未読が10秒で片付いた
- #6 MCP vs GAS — Chatwork自動化の「正解」はどっちか
- #7 コンタクト承認をn8nで自動化しようとしたら、3つの罠にハマった
- #8 ChatworkにAIチームを住まわせたら、勝手に会話が始まった
- #9 Chatwork 8ルームの全メッセージからFAQ429件を自動抽出した
- #10 Webhook署名検証を入れたら全メッセージが消えた(この記事)
- #11 過去メッセージを全件取得しようとしたら、APIの「100件の壁」にハマった
- #12 Chatwork APIの「既読」は自分で制御できる
- #13 Chatwork APIのファイル機能、使ったことある?
- #14 n8nで全ルーム巡回
- #15 タスク機能をAPIで使い倒す
- #16 MCPを2アカウント同時接続したら、仕事用と事務局用が1画面で回った
- #17 【世界初かもしれない】ChatworkでClaude Code Channelsを実装してみた
- #18 Chatwork × Dify × GASで問い合わせ回答を自動提案する
- #19 RelationMapを夜間バッチで毎日自動更新する
- #20 17記事書いて見えた、Chatwork APIエコシステムに足りないもの
- #21 Googleフォームの回答をChatworkに自動投稿するGAS
- #22 Chatworkの会話を毎日AIが要約してくれる仕組みをn8nで作った話
- #23 chatwork-cliを入れたら、シェルからChatworkが操作できて世界が変わった
- #24 ChatworkのWebhookをn8nで受けるなら、HMAC署名検証は必ずやれ
- #25 Chatwork × GAS × Claude Codeで会員制講座の運用を自動化した