HMAC認証

KARTE の Webhook では、データ改ざんの検知や送信者の確認 (なりすまし防止) のために署名をヘッダーに添付して送信しています。署名にはアプリ作成時に発行されたシークレットキーを使用して生成します。

署名の検証

KARTE から送信されるリクエストには署名として X-Karte-Signature HTTP ヘッダーが含まれています。このヘッダーには HMAC-SHA256 キー付きのハッシュがついています。このハッシュを用いて検証を行います。

署名を検証するには、X-Karte-Request-Timestamp HTTP ヘッダーの値とリクエストの本文をコロン(:)で連携した文字列に対して、アプリに発行されるシークレットキーを使用して、SHA256 ハッシュを計算します。この計算した結果と、X-Karte-Signature HTTPヘッダーの値が一致するかで検証を行います。(具体的な検証手順は後述の「検証手順」をご確認ください)。

また、 X-Karte-Request-Timestamp が有効期限が切れていないかも検証することで、リプレイ攻撃を防ぐこともできます。 X-Karte-Request-TimestampUnix時間 で表され、Webhook のリクエストが送信される直前のタイムスタンプとなります。

Webhook の HTTP ヘッダーに含まれる値

  • X-Karte-Signature: HMAC-SHA256 キー付きのハッシュ
  • X-Karte-Request-Timestamp : Webhook リクエスト送信時のタイムスタンプ(秒)

検証手順

  1. 「API v2 設定」で作成したアプリから「Client Secret」を取得します。ハッシュを計算するときのキーとなります。ここでは例としてシークレットの値を「KarteClientSecret」とします。
    【「API v2 設定」の起動経路】グローバルメニュー > ストア > API v2 設定

  2. Webhook で送信されたリクエストの HTTP ヘッダーから X-Karte-Request-Timestamp の値を取得します。例として、取得したタイムスタンプは 1612240200 とします。

  3. タイムスタンプが一定時間以上経過していないことを確認します。
    有効期限を5分とした場合、現在時刻の Unix 時間が 、1612240200300(5分を秒に換算) を足し合わせた数を超えていないかをチェックします。経過していた場合はリクエストを破棄します。

  4. タイムスタンプとリクエストの本文をコロン(:)を使って連結し署名対象文字列を作成します。
    リクエストの本文を {"user_id":XXXX,"api_key":XXXX} とした場合、署名対象文字列は 1612240200:{"user_id":XXXX,"api_key":XXXX} となります。タイムスタンプ、リクエストの本文の順で連結をします。

  5. 署名対象文字列に対して、シークレットキーを使用して、SHA256 ハッシュを計算をします。計算したハッシュは Base64 でエンコーディングします。例で上げた値の場合、OTBjNDJhYjgyZTY4Zjg5ZmU3YWZjNDc4NWZlZDM2NGUzMmMyMjMwMjdjOWEzMDg1YzUyN2YwYjViNTAwNTFmOA== のような形で出力されます。

  6. 生成したハッシュと X-Karte-Signature HTTP ヘッダーの値が一致することを確認します。一致したものは KARTE から送られてきたリクエストと判断できます。

サンプルコード

const crypto = require('crypto');
const secret = 'YOUR APP SECRET';

const requestTimestamp = req.getHeader('X-Karte-Request-Timestamp');
// 有効期限の検証(5分以内)
if ((parseInt(requestTimestamp, 10) + 5 * 60) * 1000 < new Date().getTime()) {
    throw new Error('expired timestamp');
}

const signature = req.getHeader('X-Karte-Signature');
if (!signature) {
  throw new Error('not authorized');
}

const reqBody = JSON.stringify(req.body);

const stringToSign = requestTimestamp + ':' + reqBody;
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(stringToSign).digest('base64');

if (signature !== digest) {
  throw new Error('invalid auth');
}