🔨

PayPayのOpen API用HMACの実装をJavaScriptで試してみた

2022/09/12に公開

はじめに

  • PayPayのOpen APIを通し、加盟店はPayPayを利用したオンライン決済機能を実装することが可能です。
  • Open APIを利用する方法は大きく分けて2つあります。
    • HTTPエンドポイントを直接コール方法
    • SDKを通してコールする方法
  • 直接コールの場合には、HMACを実装する必要があります。
  • 今回、HMACの実装を初めて行ったので自身の備忘録的にまとめてみることにしました

前提

  • Macを利用
  • JavaScriptを利用
  • crypto-jsを使用

結論

サンプルコード

const cryptoJS = require("crypto-js");

const SAMPLE_API_KEY = "APIKeyGenerated";
const SAMPLE_API_KEY_SECRET = "APIKeySecretGenerated";
const SAMPLE_REQUEST_URL = "/v2/codes";
const SAMPLE_REQUEST_METHOD = "POST";
const SAMPLE_EPOCH = 1579843452;
const SAMPLE_NONCE = "acd028";
const SAMPLE_REQUEST_BODY = {
  sampleRequestBodyKey1: "sampleRequestBodyValue1",
  sampleRequestBodyKey2: "sampleRequestBodyValue2",
};
const SAMPLE_REQUEST_CONTENT_TYPE = "application/json;charset=UTF-8;";

const body = SAMPLE_REQUEST_BODY;
const jsonified = JSON.stringify(body);

const step1 = () => {
  let contentType;
  let payloadDigest;

  if (jsonified) {
    contentType = SAMPLE_REQUEST_CONTENT_TYPE; // "application/json;charset=UTF-8;"
    payloadDigest = cryptoJS.algo.MD5.create()
      .update(contentType)
      .update(jsonified)
      .finalize()
      .toString(cryptoJS.enc.Base64);
  } else {
    // bodyが無い場合には固定で"empty"を指定する
    contentType = "empty";
    payloadDigest = "empty";
  }
  return payloadDigest;
};

const step2 = (hash) => {
  const requestUrl = SAMPLE_REQUEST_URL;
  const httpMethod = SAMPLE_REQUEST_METHOD;
  const nonce = SAMPLE_NONCE;
  const epoch = SAMPLE_EPOCH;
  const contentType = SAMPLE_REQUEST_CONTENT_TYPE;

  return [requestUrl, httpMethod, nonce, epoch, contentType, hash].join("\n");
};

const step3 = (dataToSign) => {
  const hashed = cryptoJS.HmacSHA256(dataToSign, SAMPLE_API_KEY_SECRET);
  const hashed64 = cryptoJS.enc.Base64.stringify(hashed);
  return hashed64;
};

const step4 = (macData, hash) => {
  const headList = [SAMPLE_API_KEY, macData, SAMPLE_NONCE, SAMPLE_EPOCH, hash];
  const header = headList.join(":");
  return header;
};

const main = async () => {
  const hash = step1();
  const dataToSign = step2(hash);
  const macData = step3(dataToSign);
  const header = step4(macData, hash);

  console.log(`hmac OPA-Auth:${header}`); // hmac OPA-Auth:APIKeyGenerated:NW1jKIMnzR7tEhMWtcJcaef+nFVBt7jjAGcVuxHhchc=:acd028:1579843452:1j0FnY4flNp5CtIKa7x9MQ==
};

main();

実行結果

$ node app.js 
hmac OPA-Auth:APIKeyGenerated:NW1jKIMnzR7tEhMWtcJcaef+nFVBt7jjAGcVuxHhchc=:acd028:1579843452:1j0FnY4flNp5CtIKa7x9MQ==

やってみたこと

流れを理解

公式のガイドを見て実装に必要な要件を理解してみました。その上で、ガイドに記載のパラメータをそのまま利用して、例にあるHMAC Auth ヘッダーが出るようにしていきます。

  • 必要なパラメータを変換し、HMAC Auth ヘッダー形式にする
  • 必要なパラメータは下記
key value
API Key secret PayPayから発行されたAPI Key secret
Request URI リクエストURL
Request method HTTPメソッド
Epoch 現在のエポックタイムスタンプ(秒)
Nonce ランダムに生成された文字列。Note : 任意の文字列、length 8を推奨。
Request body リクエストで渡されるbody。
Request content type リクエストヘッダーで渡されるコンテンツタイプ。
hash (MD5(Request body, Request content type)) リクエストの本文とコンテンツタイプのハッシュ。
  • HMAC Auth ヘッダーの形式は次の通り
    • hmac OPA-Auth:APIKeyGenerated:NW1jKIMnzR7tEhMWtcJcaef+nFVBt7jjAGcVuxHhchc=:acd028:1579843452:1j0FnY4flNp5CtIKa7x9MQ==
    • hmac OPA-Auth:までは固定値です

事前準備

HMACヘッダーを生成に必要なパラメータを定義します。

今回は、純粋にHMACヘッダーの作成ロジックだけを理解したいのでサンプルの値をそのまま利用します。値は固定でセットします。

なお、実際には、PayPay for developersにサインアップし、API KeyやAPI Key Secretを取得してセットする必要がありますし、どんなAPIをどうコールしたいかによってはRequest URIやRequest bodyは変わることになります。

上記前提のもと、まずは各種入力パラメータを初期化します。

// 認証情報
const SAMPLE_API_KEY = "APIKeyGenerated";
const SAMPLE_API_KEY_SECRET = "APIKeySecretGenerated";

// リクエスト毎に設定する情報
const SAMPLE_REQUEST_URL = "/v2/codes";
const SAMPLE_REQUEST_METHOD = "POST";
const SAMPLE_EPOCH = 1579843452;
const SAMPLE_NONCE = "acd028";
const SAMPLE_REQUEST_BODY = {
  sampleRequestBodyKey1: "sampleRequestBodyValue1",
  sampleRequestBodyKey2: "sampleRequestBodyValue2",
};
const SAMPLE_REQUEST_CONTENT_TYPE = "application/json;charset=UTF-8;";
const body = SAMPLE_REQUEST_BODY;
const jsonified = JSON.stringify(body);

また、自分流の書き方にはなりますが、先にメインルーチンを仮で書いておきます。

最終的にはconsole.logで書いたようなヘッダーを作り出したい。

const main = async () => {
  const hash = step1();
  const dataToSign = step2(hash);
  const macData = step3(dataToSign);
  const header = step4(macData, hash);

  console.log(`hmac OPA-Auth:${header}`); // hmac OPA-Auth:APIKeyGenerated:NW1jKIMnzR7tEhMWtcJcaef+nFVBt7jjAGcVuxHhchc=:acd028:1579843452:1j0FnY4flNp5CtIKa7x9MQ==
};

main();

bodyとcontent-typeのHash化

実際にヘッダー作成に入ります。

最初のステップでは、bodyとcontent-typeをHash化したものを作成します。

後学のために、bodyがない場合には固定文字列で empty を入れる必要があるのでその場合も実装しておきます。

const step1 = () => {
  let contentType;
  let payloadDigest;

  if (jsonified) {
    contentType = SAMPLE_REQUEST_CONTENT_TYPE; // "application/json;charset=UTF-8;"
    payloadDigest = cryptoJS.algo.MD5.create()
      .update(contentType)
      .update(jsonified)
      .finalize()
      .toString(cryptoJS.enc.Base64);
  } else {
    // bodyが無い場合には固定で"empty"を指定する
    contentType = "empty";
    payloadDigest = "empty";
  }
  return payloadDigest;
};

SHA256

2つ目のステップからは、bodyとcontent-type以外のHash化したものを作っていきます。

実際のHash化は3つ目のステップで行うため、ここではHash化のためのinputを作っていきます。

それぞれの値を \n で連結します。

const step2 = (hash) => {
  const requestUrl = SAMPLE_REQUEST_URL;
  const httpMethod = SAMPLE_REQUEST_METHOD;
  const nonce = SAMPLE_NONCE;
  const epoch = SAMPLE_EPOCH;
  const contentType = SAMPLE_REQUEST_CONTENT_TYPE;

  return [requestUrl, httpMethod, nonce, epoch, contentType, hash].join("\n");
};

HMACオブジェクト化

3つ目のステップでは、2つ目で作成した値と、API Secretを組み合わせてHMACオブジェクトを生成します。

const step3 = (dataToSign) => {
  const hashed = cryptoJS.HmacSHA256(dataToSign, SAMPLE_API_KEY_SECRET);
  const hashed64 = cryptoJS.enc.Base64.stringify(hashed);
  return hashed64;
};

ヘッダー化

最後に、ここまで用意した変数を組み合わせて hmac OPA-Auth:${header} の右側の部分を生成します。

const step4 = (macData, hash) => {
  const headList = [SAMPLE_API_KEY, macData, SAMPLE_NONCE, SAMPLE_EPOCH, hash];
  const header = headList.join(":");
  return header;
};

うごかしてみる

最初に作ったメインルーチンを起点に、各stepを通してHMAC Auth ヘッダーを実際に生成してみます

$ node app.js 
hmac OPA-Auth:APIKeyGenerated:NW1jKIMnzR7tEhMWtcJcaef+nFVBt7jjAGcVuxHhchc=:acd028:1579843452:1j0FnY4flNp5CtIKa7x9MQ==

ガイドに記載のサンプル値と一致したため成功です。

もし、ここで1文字でも違う値が出てしまったら何かが間違っています。各ステップをおさらいしてみましょう。

最後に

HMACの実装はケアレスミスでハマることがよくありますが、どこまではうまく行っている、という切り分けのためにも、流れの理解やステップ分けを整理しておくことで、トラブルシュートの時間を最小化することができると思います。ということで、とりあえず一度実装してみるというのはよいと思いますので参考になれば幸いです。

参考

Discussion