dosyaのWebhook署名を検証する

変更されていないリクエスト本文に対してX-Dosya-SignatureのタイムスタンプとHMACを検証し、再送されたリクエストを拒否して、公開されたシークレットを安全にローテーションします。

最終確認日

スクリーンショットは英語版の画面です。

JSONを解析したり処理を開始したりする前に、すべてのWebhookを検証してください。dosyaはエンドポイントのwhsec_シークレットで正確なリクエスト本文に署名し、その結果をX-Dosya-Signatureで送信します。有効な署名はそのシークレットの所持を証明しますが、後続のジョブに必要な認可チェックの代わりにはなりません。

署名の仕様

ヘッダーは次の正確な形式です。

t=UNIX_SECONDS,v1=64_LOWERCASE_HEX_CHARACTERS

HMAC-SHA256(secret, timestamp + "." + raw_body)を計算し、32バイトのダイジェストをv1と定数時間の操作で比較してください。raw_bodyは変更されていないHTTPエンティティのバイトデータを意味します。JSONの解析と再シリアライズ、空白の変更、Unicodeの正規化、改行コードの変換は異なるダイジェストを生成します。

再送の防止にはtの値を使用してください。dosyaのAPIリファレンスは、受信側の現在時刻から300秒を超えて離れた署名を拒否することを推奨しています。受信側の時計を同期し、イベントをキューに追加する前にタイムスタンプを評価してください。この5分間の確認を行うのは受信側です。dosyaはアプリケーションが確認を実施したかを把握できません。

dosyaのWebhook本文を保持し、タイムスタンプを確認してHMAC-SHA256を計算し、その後にイベントを解析する検証手順。
まずバイトデータを取得し、古いタイムスタンプを拒否して固定長のダイジェストを比較し、認証成功後にのみJSONを解析します。スクリーンショットを原寸大で開く。

手順

元の本文を含むBufferを取得できるようルートを設定してください。フレームワークのJSONミドルウェアは本文を消費または変換することが多いため、全体に適用されるJSONパーサーより前にこのルートで生の本文を扱う設定を行います。

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyDosya(rawBody, signatureHeader, secret, nowSeconds) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader ?? '');
  if (!match) return { ok: false, reason: 'malformed signature' };

  const timestamp = Number(match[1]);
  const now = nowSeconds ?? Math.floor(Date.now() / 1000);
  if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > 300) {
    return { ok: false, reason: 'stale timestamp' };
  }

  const expected = createHmac('sha256', secret)
    .update(match[1] + '.')
    .update(rawBody)
    .digest();
  const received = Buffer.from(match[2], 'hex');
  const valid = received.length === expected.length && timingSafeEqual(received, expected);
  return valid ? { ok: true, timestamp } : { ok: false, reason: 'signature mismatch' };
}

rawBodyには解析済みのオブジェクトではなくBufferを渡してください。関数がok: trueを返したらJSONを解析し、期待する最上位のid、type、created、workspace_id、dataを確認して、イベントIDを一意の重複排除ストアに書き込みます。永続的な受け入れ後に2xxを返してください。dosyaは2xx以外のレスポンスとタイムアウトを再試行するため、正しく認証する受信側にも同じイベントが再び届く場合があります。

正常系と異常系をテストする

  1. 制御されたエンドポイントの連携 → Webhookを開き、テストイベントを送信を選択してください。
  2. 受信側が署名付きリクエストを受け入れ、X-Dosya-Event-Id、X-Dosya-Delivery-Id、イベントの種類、タイムスタンプを記録することを確認します。署名やシークレットはログに記録しないでください。
  3. ローカルテストでヘッダーを変えずに記録した本文の1バイトを変更します。JSONの処理前に検証が失敗する必要があります。
  4. 別のシークレット、欠落したヘッダー、不正なhex値、許可範囲から301秒外れたタイムスタンプをテストしてください。いずれも処理をキューに追加してはいけません。
  5. 有効なイベントを再配信し、一意のイベントID制約が後続操作の重複を防ぐことを確認してください。

dosyaの成功バッジはエンドポイントが2xxを返したことを意味します。受信側が署名を確認した証拠にはなりません。有効と無効の両方のケースについて、受信側のテストを判断の根拠として保持してください。

署名の不一致を調べる

症状修正方法
デプロイ後にすべての配信が失敗するルートがJSONまたはフォームのミドルウェアより前にバイトデータを受け取り、対象のエンドポイントのシークレットを使用することを確認してください。
非ASCIIや整形されたペイロードだけが失敗するHMACの入力からテキストのデコードと再シリアライズを取り除き、受信したバイトデータをハッシュしてください。
有効な通信が古いと判定される受信側の時計を同期し、ミリ秒ではなくUnix秒を比較してください。
保存した古いリクエストが通ってしまう300秒のタイムスタンプ範囲を追加し、イベントIDを永続化してください。
シークレットをローテーションの直後から失敗する新しく表示されたシークレットを設定してください。以前の値では新しい配信を直ちに検証できなくなります。

シークレットはエンドポイントの作成時またはローテーション時にのみ署名シークレットに表示されます。失った場合はシークレットをローテーションが必要です。古い値は併用期間なしで使えなくなります。ローテーションのエンドポイントはユーザーごとに1時間あたり10回までで、新しい値は1回だけ返されます。シークレットがコード管理、ログ、スクリーンショット、チャットに漏れた場合は、公開されたコピーを削除するだけでなくローテーションしてください。

dosyaのZapier設定ページ。カスタムWebhookエンドポイントに表示される署名シークレットとは別です。
カスタムエンドポイントのシークレットは**Integrations → Webhooks**から表示されます。ZapierのAPIキーは別の認証情報であり、HMACの検証に使用してはいけません。スクリーンショットを原寸大で開く。

期待される結果

保存済みエンドポイントシークレットで署名された、時刻が有効で変更されていないリクエストが受け入れられます。署名がない、不正、古い、変更された、または誤った署名のリクエストは状態を変更する前に拒否されます。同じ有効なイベントが繰り返された場合は業務処理を繰り返さずに応答します。

関連ガイド

この記事は役に立ちましたか?

評価の結果を読み込んでいます…

投票すると、回答を変更できるように、必要な投票用Cookieが最長180日間保存されます。

質問と回答

このガイドの手順について質問できます。質問は確認後に公開されます。

公開の質問には、アカウントの詳細、認証情報、非公開ファイルを含めないでください。これらについては 非公開サポート をご利用ください。

質問を読み込んでいます…

    dosyaアカウントにサインインすると、質問を投稿できます。

    やり直す前に結果を確認

    操作をもう一度確認する必要があるときは、次の共通の確認事項をご利用ください。