はじめに
前回の記事では、Cloudflare Workersを使ったSlack Bot(技術記事下書きBot)の実装方法を紹介しました。その中で触れた署名検証について実装していきます。
なぜ署名検証が必要なのか
前回作成したBotのように、公開エンドポイントでSlackからのリクエストを受け取る場合、そのURLが第三者に知られると、偽のリクエストを送信される可能性があります。
このリスクを防ぐために、Slackの署名検証機能を実装する必要があります。
署名検証の仕組み
Slackの署名検証は以下の流れで動作します
- SlackがリクエストボディとタイムスタンプをHMAC-SHA256で署名
- 署名をリクエストヘッダー(
x-slack-signature
)に含めて送信 - Bot側で同じ方法で署名を計算し、一致するか確認
検証は以下の流れで行います
- (事前準備)Slack API設定画面から、Signing Secretを取得する
- リクエストからタイムスタンプを取得する
リプレイ攻撃から保護するために、公式の例に則って検証時刻とタイムスタンプのずれが5分以上あれば以降の処理を中断します -
{バージョン番号}:{タイムスタンプ}:{リクエスト本文}
の形式で連結する - 署名シークレットをキーとして使用して連結文字列をハッシュし、ダイジェストを取得する
- 結果をリクエストヘッダーと比較する
タイミング攻撃から保護するため、timingSafeEqual
を利用して比較します
詳しくは公式ドキュメントを参照してください
実装
署名検証処理
import crypto from 'node:crypto';
import { Buffer } from 'node:buffer';
/**
* Slackのリクエストを検証
* @param headers リクエストヘッダー
* @param body リクエストボディ
* @param signingSecret Slackのシグネチャ
* @returns リクエストが有効かどうか
*/
function isValidSlackRequest(headers: Headers, body: string, signingSecret: string): boolean {
// 1. 必須ヘッダーの存在確認
const timestamp = headers.get('x-slack-request-timestamp');
const signature = headers.get('x-slack-signature');
if (!timestamp || !signature) {
console.warn('Missing required Slack headers');
return false;
}
// 2. タイムスタンプ範囲検証(5分以内)
const timestampNum = parseInt(timestamp, 10);
const now = Math.floor(Date.now() / 1000);
const timeDiff = Math.abs(now - timestampNum);
if (timeDiff > 60 * 5) {
console.warn('Request timestamp too old or future', { timeDiff });
return false;
}
// 3. 署名形式の事前チェック
if (!signature.startsWith('v0=') || signature.length !== 67) {
// v0= + 64文字のhex
console.warn('Invalid signature format');
return false;
}
// 4. 署名検証
const baseString = `v0:${timestamp}:${body}`;
const computedSignature = 'v0=' + crypto.createHmac('sha256', signingSecret).update(baseString).digest('hex');
// 5. タイミングセーフ比較
try {
return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature));
} catch (error) {
console.warn('Signature comparison failed', error);
return false;
}
}
利用側
/**
* handler
*/
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): PromiseResponse> {
const rawBody = await request.text(); // 追加
// 今回追加した署名検証
if (!isValidSlackRequest(request.headers, rawBody, env.SLACK_SIGNING_SECRET)) {
console.warn('Unauthorized – bad Slack signature', {
timestamp: request.headers.get('x-slack-request-timestamp'),
signature: request.headers.get('x-slack-signature'),
});
return new Response('Unauthorized – bad Slack signature', { status: 401 });
}
const url = new URL(request.url);
if (request.method === 'POST' && url.pathname === '/slack/events') {
// const rawBody = await request.text(); // 冒頭に移動
const params = new URLSearchParams(rawBody);
const responseUrl = params.get('response_url') ?? '';
const text = params.get('text') ?? '';
// 非同期バックグラウンド実行
ctx.waitUntil(handleSlackEvent(text, responseUrl, env));
// 即時レスポンス(Slackタイムアウト回避)
return new Response(`✏️プロンプトを受け取りました。記事生成中です。\nprompt:\n${text}`);
}
return new Response('Not Found', { status: 404 });
},
} satisfies ExportedHandlerEnv>;
追加設定
wrangler.jsonc
"compatibility_flags": ["nodejs_compat"],
"compatibility_date": "2024-09-23", // 元の設定値を書き換える
Node.jsランタイムAPIのcryptoを使うため必要な設定です。
詳しくは公式ドキュメントを参照してください。
環境変数
型の追加
interface Env {
OPENAI_API_KEY: string;
GITHUB_TOKEN: string;
GITHUB_REPO: string;
GITHUB_BRANCH: string;
SLACK_SIGNING_SECRET: string; // 追加
}
ターミナルで以下を実行し、環境変数を設定します
npx wrangler secret put SLACK_SIGNING_SECRET
入力欄表示後、SlackAPI設定画面より取得したSigning Secret
を入力します。
設定後、Cloudflare Workersの設定画面から項目が確認できるようになっていればOKです。
デプロイ
前回記事同様、以下のコマンドでデプロイできます。
まとめ
前回記事に続き、SlackBotをCloudflareWorkers環境にデプロイする際必要な署名検証機能を追加しました。
これにより安全で実用的なBotになります。
参考
Views: 0