コンテンツにスキップ

Aikido Webhooks

Aikido から届く受信 Webhook が実際に Aikido から送信されたものであり、そのペイロードが改ざんされていないことを検証するために、Aikido はあなたと共有したシークレットで署名されたペイロードのハッシュを使用します。

Webhook のスキーマはこちらの API ドキュメントで確認できます。

  1. webhooks integration ページに移動します

  2. 右側のテーブルのすぐ上にある「Add secret」をクリックしてシークレットを作成します。

    新しい Webhook の登録を促す、空の Webhook ダッシュボード。

  3. Webhook シークレットが作成されると、モーダルが表示され、Webhook シークレットをコピーして安全に保存できます。このシークレットは安全に保管し、コードリポジトリにコミットしないことが重要です

設定したイベントに対して Aikido が Webhook を送信する際、そのイベントのペイロードのハッシュを作成し、先ほど生成したシークレットで署名します。この一意のハッシュは X-Aikido-Webhook-Signature リクエストヘッダーに含まれます。これにより、Webhook とそのペイロードが本物であることを検証できます。

リプレイ攻撃を防ぐため、HTTP リクエストを送信する直前のエポックタイムスタンプを Webhook ペイロードに含めています。このタイムスタンプは dispatched_at プロパティとして含まれ、ペイロードを検証する時点で30秒より古くないことを確認する必要があります。

使用するプログラミング言語に関わらず、受信 Webhook を検証するプロセスは次のようになります。

  1. ペイロードが有効な JSON 文字列であることを確認する
  2. X-Aikido-Webhook-Signature リクエストヘッダーから署名を取得する
  3. リクエストボディを JSON 文字列にパースし直す
  4. 文字列化されたリクエストボディから sha256 アルゴリズムで hmac ダイジェストを作成し、Aikido から取得した Webhook シークレットで署名する
  5. リクエストヘッダーの署名が、今生成したダイジェストと一致することを検証する
  6. プロパティの dispatched_at エポックタイムスタンプが30秒より古くないことを検証する

以下では、express フレームワークを使用してハッシュを検証する方法を示す疑似 JavaScript コードを紹介します。これはミドルウェアとして組み込む必要があり、値が有効かどうかについてはさらに多くの検証を行う必要があります。

const { createHmac } = require('node:crypto');
const express = require('express');
const bodyParser = require('body-parser');
const PORT = 4000;
const app = express();
app.use(bodyParser.json());
const isIncomingWebhookValid = (payload, signature) => {
// get the raw webhook secret from the environment variables
const aikidoWebhookSecret = process.env.AIKIDO_WEBHOOK_SECRET;
// lets create the hmac instance
const hmac = createHmac('sha256', aikidoWebhookSecret);
// lets convert the payload object to a JSON string
const rawPayload = JSON.stringify(payload);
// update the hmac content with the stringified payload
hmac.update(rawPayload);
// calculate the digest of the hmac content and return it as a hex value
const payloadDigest = hmac.digest('hex');
// if digest does not match the provided signature, the webhook is invalid
if (payloadDigest !== signature) return false;
// get the current epoch timestamp
const currentEpochTimestamp = Math.floor(new Date().getTime() / 1000);
// if the dispatched_at epoch timestamp from the payload is longer than 30 seconds old, the webhook is invalid
if ((currentEpochTimestamp - (payload['dispatched_at'] ?? 0)) > 30) return false;
// webhook passed all checks and is valid
return true;
}
app.post('/webhooks/aikido/issue-created', async (req, res) => {
const isValid = isIncomingWebhookValid(req.body, req.headers['X-Aikido-Webhook-Signature']);
if (!isValid) {
throw new Error(`The request signature is invalid`)
}
// your business code
res.status(204);
});
app.listen(PORT, () => {
console.log(`server listening on port ${port}`);
});