日本語

Aikido Webhooks

Webhookの認証

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

Aikidoは常に次のIPアドレスからWebhookリクエストを送信します: 52.18.113.172

Webhookスキーマ

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

Webhookシークレットの生成

  1. Webhook連携ページに移動します

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

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

  3. Webhookシークレットが作成されると、モーダルが表示されます。ここで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}`);
});