> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frictio.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhookの検証

> 受信したWebhookが正当なものか署名で検証する

FrictioはWebhookの配信に[Svix](https://www.svix.com/)を使用しています。
機密性を担保するため、ペイロードデータのログは送信後に削除されます。

受信側では、送られてきたリクエストが本当にFrictioからのものかを署名で検証することを推奨します。
検証には、Svix公式ライブラリを使う方法と、自前で署名を計算する方法があります。

## 1. Svix公式ライブラリによる検証（推奨）

公式ライブラリを使うと、署名とタイムスタンプの検証を一括で行えます。

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install svix
    ```

    ```python theme={null}
    from svix import Webhook, WebhookVerificationError

    secret = "your-webhook-secret"
    headers = {
        "webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
        "webhook-timestamp": "1614265330",
        "webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
    }

    wh = Webhook(secret)
    try:
        verified_payload = wh.verify(payload, headers)
    except WebhookVerificationError:
        raise ValueError("Webhook verification failed!")
    ```
  </Tab>

  <Tab title="Node.js">
    ```bash theme={null}
    npm install svix
    # または
    yarn add svix
    ```

    ```javascript theme={null}
    import { Webhook } from "svix";

    const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

    // これらはすべてサーバーから送信されたものです
    const headers = {
      "webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
      "webhook-timestamp": "1614265330",
      "webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
    };
    const payload = '{"test": 2432232314}';

    const wh = new Webhook(secret);
    // エラー時は例外、成功時は検証済みのコンテンツを返します
    wh.verify(payload, headers);
    ```
  </Tab>

  <Tab title="Java">
    Gradleの場合、プロジェクトのビルドファイルに以下の依存関係を追加します。

    ```groovy theme={null}
    implementation "com.svix:svix:0.x.y"
    ```

    Mavenの場合、`pom.xml` に以下の依存関係を追加します。

    ```xml theme={null}
    <dependency>
      <groupId>com.svix</groupId>
      <artifactId>svix</artifactId>
      <version>0.x.y</version>
    </dependency>
    ```

    ```java theme={null}
    import com.svix.Webhook;

    String secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

    // これらはすべてサーバーから送信されたものです
    HashMap<String, List<String>> headerMap = new HashMap<String, List<String>>();
    headerMap.put("webhook-id", Arrays.asList("msg_p5jXN8AQM9LWM0D4loKWxJek"));
    headerMap.put("webhook-timestamp", Arrays.asList("1614265330"));
    headerMap.put("webhook-signature", Arrays.asList("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="));
    HttpHeaders headers = HttpHeaders.of(headerMap, (a, b) -> true);

    String payload = "{\"test\": 2432232314}";

    Webhook webhook = new Webhook(secret);

    webhook.verify(payload, headers);
    // 検証失敗時は WebhookVerificationError 例外をスローします
    ```
  </Tab>

  <Tab title="Kotlin">
    Gradleの場合、プロジェクトのビルドファイルに以下の依存関係を追加します。

    ```groovy theme={null}
    implementation "com.svix.kotlin:svix-kotlin:0.x.y"
    ```

    Mavenの場合、`pom.xml` に以下の依存関係を追加します。

    ```xml theme={null}
    <dependency>
      <groupId>com.svix.kotlin</groupId>
      <artifactId>svix-kotlin</artifactId>
      <version>0.x.y</version>
    </dependency>
    ```

    ```kotlin theme={null}
    import com.svix.kotlin.Webhook

    val secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

    // これらはすべてサーバーから送信されたものです
    val headersMap = mapOf(
        "webhook-id" to listOf("msg_p5jXN8AQM9LWM0D4loKWxJek"),
        "webhook-timestamp" to listOf("1614265330"),
        "webhook-signature" to listOf("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=")
    )
    val headers = HttpHeaders.of(headersMap) { _, _ -> true }

    val payload = "{\"test\": 2432232314}";

    val webhook = Webhook(secret);

    webhook.verify(payload, headers)
    // 検証失敗時は WebhookVerificationError 例外をスローします
    ```
  </Tab>
</Tabs>

## 2. 自前での署名検証（Manual Verification）

ヘッダーとシークレットキーを用いて、受け取ったペイロードが正当なものかを自前で検証します。

### 必要なもの

* HTTPヘッダー（`webhook-id`, `webhook-timestamp`, `webhook-signature`）
  * `webhook-id`: Webhookメッセージの一意な識別子。すべてのメッセージ間で一意だが、同じWebhookが再送信された場合（以前の送信が失敗した場合など）は同じ識別子が使用される
  * `webhook-timestamp`: エポック（UNIX時間）からの経過秒数で表されたタイムスタンプ
  * `webhook-signature`: Base64でエンコードされた署名のリスト（スペースで区切られた複数の署名を含む場合がある）
* Webhookシークレットキー（Frictio管理画面で発行）
* payload（リクエストボディ）

### 手順

#### 1. 署名対象コンテンツの構築

署名に使用されるコンテンツは、id・timestamp・payloadをピリオド（`.`）で連結したものです。

```javascript theme={null}
const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
```

ここで `body` はリクエストの生（raw）ボディを指します。
署名は非常に敏感で、`body` がわずかにでも変更されると署名は完全に異なるものになります。
そのため、署名を検証する前に `body` を一切変更しないことが重要です。

#### 2. 期待される署名の算出

Webhookの署名にはHMAC（ハッシュベースのメッセージ認証コード）とSHA-256を使用しています。
署名の検証を行うには、`signedContent` を、署名シークレットのBase64部分をキーとして使い、HMAC-SHA256で署名（ハッシュ）します。

署名シークレットが `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` の場合、実際の秘密鍵として使うのは `whsec_` の後ろの部分（`MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`）です。

```javascript theme={null}
const crypto = require('crypto');

const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";

// シークレットをbase64デコードする必要があります
const secretBytes = Buffer.from(secret.split('_')[1], "base64");
const signature = crypto
  .createHmac('sha256', secretBytes)
  .update(signedContent)
  .digest('base64');

console.log(signature);
```

生成した署名は、`webhook-signature` ヘッダー内に含まれるいずれかの署名と一致している必要があります。
`webhook-signature` ヘッダーは、スペース区切りで並んだ複数の署名とそのバージョン識別子で構成されています。通常は1つだけですが、複数の署名が含まれる場合もあります。

```text theme={null}
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=
```

署名の検証を行う前に、各署名のバージョンプレフィックス（例: `v1,`）と区切り文字を取り除いてください。
また、署名の比較にはタイミング攻撃（timing attack）を防ぐために、定数時間（constant-time）で文字列を比較する方法の使用が推奨されます。

### タイムスタンプの検証

各リクエストの試行時刻を `webhook-timestamp` ヘッダーに含めて送信します。
このタイムスタンプを自身のシステムの現在時刻と比較し、許容範囲内（過去5分以内）にあることを確認してください。
これはリプレイ攻撃（replay attack）を防ぐためです。

### 署名の検証例

以下は、実装が正しく行われているか確認するために使える検証用の例です。
なお、この例ではタイムスタンプが古いため、検証に失敗する可能性があることに注意してください。

```text theme={null}
secret = 'whsec_plJ3nmyCDGBKInavdOK15jsl';
payload = '{"event_type":"ping","data":{"success":true}}';
msg_id = 'msg_loFOjxBNrRLzqYUf';
timestamp = '1731705121';

// 上記から次の署名が生成されます
signature = 'v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0=';
```

1. 署名の照合: `v1,` プレフィックスを取り除いた値と、計算した署名（`computed_signature`）を比較し、一致すればOKです
2. タイムスタンプの検証: `webhook-timestamp` が過去5分以内であるかを確認します

### 参考リンク

* [Svix公式ドキュメント: Manual Verification](https://docs.svix.com/receiving/verifying-payloads/how-manual)
* [Svix公式ドキュメント: Using Libraries](https://docs.svix.com/receiving/verifying-payloads/how)
