ドキュメント本文へスキップ
SDK

配信の検証

`verifyWebhookSignature`: 一定時間での比較、リプレイ許容ウィンドウ、そしてパース済みイベントの返却。

リクエストハンドラーでの使い方

Webhook の URL は公開されている。インターネット上の誰でも、正しい形の JSON をそこへ POST できる。したがって、署名を検証せずに payload.type を読むハンドラーは、誰でも書き込める API にほかならない。

webhook-handler.ts
import { verifyWebhookSignature } from '@openemail/sdk' export async function POST(request: Request) {  try {    const event = await verifyWebhookSignature({      payload: await request.text(),      headers: request.headers,      secret: process.env.OPENEMAIL_WEBHOOK_SECRET!,      toleranceSeconds: 300,    })     console.log(event.type, event.data)  } catch {    return new Response('bad signature', { status: 400 })  }   return new Response(null, { status: 204 })}

ボディは生のまま渡すこと。パースして再度シリアライズするとキーの順序や空白が変わり、署名が一致しなくなる。headersHeaders オブジェクト、または Node の req.headers のような素のオブジェクトを受け取り、X-OpenEmail-Signature は大文字小文字にかかわらず見つけられる。

自前の検証が取りこぼしがちな 2 点をこの関数は処理する。1 つは MAC を一定時間で比較することで、処理時間から正しい先頭部分を割り出せないようにする。もう 1 つは、前後いずれの方向でも toleranceSeconds(指定しなければ 5 分)を超えて古い配信を拒否することで、傍受されたリクエストが永久に再送可能にならないようにする。toleranceSeconds: 0 にするとリプレイ検査は無効になる。どちらの不具合も表に出ない。いずれかを抱えたハンドラーでも、思いつくかぎりのテストはすべて通ってしまう。

失敗した場合は必ず例外を送出する。X-OpenEmail-Signature ヘッダーがない、形式が t=<seconds>,v1=<hex> になっていない、タイムスタンプが許容範囲外である、署名が一致しない、のいずれの場合も同様である。成功した場合は、ボディを WebhookPayload としてパースした結果に解決するため、間違えやすい 2 度目の JSON.parse は不要である。data に型を付けるには、EmailOpenedData のような型引数を渡すこと。

globalThis.crypto.subtle を必要とするが、これは Node 20+、Bun、Deno、Cloudflare Workers のいずれも提供している。