フロントエンドのJWTレスポンスに対する公開鍵を取得する (1.0)

はじめに

JSファンクションを使って機能を呼び出す場合、JWTを取得してペイロードがPayPayからのものであることを確認する必要があります。 JWTを確認する方法、Open Payment APIサービスの呼び出し回数を減らす方法、 およびPayPayからのペイロードであることを確認する方法について、手順を追って説明します。

加盟店を登録する

PayPay OPAの利用を開始するには、事前に定められたプロセスに従ってPayPay加盟店の登録を行なってください。 このプロセスは 情報収集、手動検証、契約確認、およびクレデンシャル情報の発行から構成されます。

PayPayの加盟店として登録された後、以下の項目が設定されます。

  • api key と apiKeySecret
  • webhookの通知先となるエンドポイント
  • クライアントIPのホワイトリスト
  • 加盟店識別子(エージェントクライアントの場合)

これらの設定を管理するには、マーチャントパネルを使用するか、弊社営業担当までご連絡ください。

API認証

OPA API認証に関わることは全て API認証のページ にあります。

エラー処理

PayPay OPAはHTTPレスポンスステータスコードとOPAエラーコードを使用してリクエストの成功または失敗を示します。 これらの情報を基に、どのようなエラー対応をするべきかを判断できます。 通常、PayPay OPAは以下のHTTPレスポンスステータスコードを返します。

Response code list

Common response code

Status Code Description
200 SUCCESS Success
400 KID_NOT_FOUND システムにKIDが存在しません。
401 UNAUTHORIZED 有効なAPI keyとsecretがセットされていません.
429 RATE_LIMIT このステータスコードは、クライアントが一定期間内に送信したリクエストが多すぎたため、速度制限に達したことを示します。 リクエストの送信を遅くするか、上限引き上げをご要望の旨を弊社までご連絡ください。
500 INTERNAL_SERVER_ERROR この状態コードは、PayPay側で何か問題が発生したことを示します。
503 MAINTENANCE_MODE 定期メンテナンスです。
503 SERVICE_UNDER_MAINTENANCE 定期メンテナンスです。

API共通リクエストID

基本的に、すべてのAPIレスポンスには X-REQUEST-ID がレスポンスヘッダーとして含まれます(一部例外を除きます)。 PayPayへお問い合わせの際は、このリクエストIDをご提示ください。

フォーマット: 英数字とハイフン(最大64文字)

例:

OPA45F681001AEF4605B2A50939F611F4B8

JWTを自分で確認する方法の説明

フロントエンドからの各レスポンスは、サーバーが取得した公開鍵/秘密鍵のランダムなペアを使用して暗号化されています。 JWTからバックエンドの応答を含むペイロードを取得できます。 JWTトークンを復号化するために、KIDを指定して公開鍵を取得するAPIを提供します。 KIDはレスポンスのJWTから取得できます。

KIDを取得したら、このドキュメントで説明している「Retrieves a public key given a KID」APIを呼び出して、KIDの公開鍵を取得します(セット返却します)。 公開鍵を取得したら、毎回get public key APIを呼び出す必要がないように、KIDと公開鍵をペアで保存してください。

注:これらのペアは常に日本標準時で毎週火曜日の15:00に更新されます。したがって、毎週火曜日の15:00に削除してください。

Get public key flow

プログラムでJWTを確認する方法

サーバーから受信したJWTを確認するには、まずjwtトークンからKIDを取得する必要があります。 たとえば、Javaでは次のようにしてKIDを取得できます。

final DecodedJWT decodedJWT = JWT.decode(payload);
final String kid = decodedJWT.getKeyId();

KIDを取得したら、公開鍵が保存されていない場合にのみ、 publicKey APIを呼び出します。

// setup...
final CacheServiceImplementation cacheServiceImplementation;
// ...
// inside some get public key method
String publicKey;
// we only call v1/publicKey if we don't know the public key.
if (cacheServiceImplementation.contains(KID)) {
  // do not call publicKey api and instead retrieve it from the cache
  publicKey = cacheServiceImplementation.get(KID)
}
else {
  // call the publicKeyApi since we don't have the KID that contains a public key value
  // curl example:
  // curl "http://stg-api.paypay.ne.jp/v1/publicKey?kid=0b08710e-e8d6-4c4d-b46f-27509012ac21" \
  // -H "Authorization: hmac OPA-Auth:a_1obUXXXXX_XXXX:BY8NnUuXXXXX_XXXXae8OXXXXX_XXXXP5Hhlx8+b0=:98aa04:1588839957:empty"
  publicKey = Optional.ofNullable(restTemplate.exchange(
    "http://stg-api.paypay.ne.jp/v1/publicKey/opa/api/v1/publicKey?kid={kid}", HttpMethod.GET,
    new HttpEntity<>(headers),OPAPublicKey.class,kid,
    )).map(OPAPublicKey::getData).map(OPAData::getPublicKey).orElseThrow(NoPublicKeyException::new);
  // save the KID associated with the public and expire it next tuesday at 15:00.
  cacheServiceImplementation.put(KID, public)
}

注:上記のcurlは例であり、マスクをしています。 実際はAPIキーとシークレットを使用してhmacを生成して設定する必要があります。

APIから下記のような応答が返されます。

{
    "resultInfo": {
        "code": "SUCCESS",
        "message": "Success",
        "codeId": "08100001"
    },
    "data": {
        "publicKey": "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi47XWBtFBi944WwTwkTm/yJeYqSXhKxnPMVa0M+PW7FbTqGQ2deeHAx9lC5MxkdnJfRPeOI+Zy7l9tJAN510gBin7QGH879B/5lxu887DuaN8ERn8T9g/2DXAzPq2roWCHtBkcfuHJtqBybVsBGZDrC54JutBDOl6ACnUYJbCIag6UBCwRkIxkA/9yRDGS6+uJkhpqPWFQdfwd8+8JP9D5c+dDEN9pwL6/X2kZkFKOdyheOiNrqjShokaNDYqu3vZbI40HI2b7SgTsF4elsyv5TBBjjTc9NfsLHELKEseCA9djDHNZksKHwU+fgXwY2bp+N/UdVW+BLqKNUDjkZGyQIDAQAB-----END PUBLIC KEY-----"
    }
}

ペイロードを確認する方法の前に、JWTの構造を説明します。

{
  "iss": "",
  "exp": 1589531351,
  "aud": "a_XXXXXXX",
  "iat": 1589530451,
  "payload": "{}"
}

audienceにはあなたのClientIdが設定されます。 expはJWTの有効期限です。 ペイロードはにはレスポンスの本文が設定されています。

次にペイロードを確認します。 まず、有効な公開鍵であることを確認するために以下を実行します。

final String yourPayload = ...
final String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi47XWBtFBi944WwTwkTm/yJeYqSXhKxnPMVa0M+PW7FbTqGQ2deeHAx9lC5MxkdnJfRPeOI+Zy7l9tJAN510gBin7QGH879B/5lxu887DuaN8ERn8T9g/2DXAzPq2roWCHtBkcfuHJtqBybVsBGZDrC54JutBDOl6ACnUYJbCIag6UBCwRkIxkA/9yRDGS6+uJkhpqPWFQdfwd8+8JP9D5c+dDEN9pwL6/X2kZkFKOdyheOiNrqjShokaNDYqu3vZbI40HI2b7SgTsF4elsyv5TBBjjTc9NfsLHELKEseCA9djDHNZksKHwU+fgXwY2bp+N/UdVW+BLqKNUDjkZGyQIDAQAB-----END PUBLIC KEY-----";
final String key = publicKey.substring("-----BEGIN PUBLIC KEY-----".length(), publicKey.indexOf("-----END PUBLIC KEY-----"));
byte[] decodedPublicKey = Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8));
X509EncodedKeySpec spec = new X509EncodedKeySpec(decodedPublicKey);
RSAPublicKey publicKey1 = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec);
Algorithm algorithm = Algorithm.RSA256(publicKey1, null);
String payload;
try {
  // verify with the payload.
  DecodedJWT decodedJWT = JWT.require(algorithm).build().verify(yourPayload);
  String audClientId = decodedJWT.getClaims().get("aud")
  if (!audClientId.equals("MY_CLIENT_ID")) {
    throw new MismatchClientIdException();
  }
  payload = decodedJWT.getClaims().get("payload");
} catch (Exception e) {
  // handle invalid signature or any other error.
}

ペイロードを取得したら、それをObjectまたはMapに変換し、データフィールド内のキー「responseValidTill」を確認する必要があります。

(例:payloadMap.get("data").get("responseValidTill") )

responseValidTill(エポック時間で表された時間)がレスポンスを受信した時刻よりも前の場合、このペイロードは無効となります。

注:JWTは常に作成後15分で期限切れになります。

KIDと公開鍵をともに保存して、OPAを毎回呼び出さないようにしてください。KIDは火曜日の15:00に新しく作成されるため、毎週取得する必要があります。

公開鍵を取得する

Retrieves a public key given a KID

フロントエンドのJWTから返された署名付きレスポンスよりKIDを取得したら、このAPIを呼び出して公開鍵を取得する。

query Parameters
kid
required
string

レスポンスから取得したKID

Responses

Response samples

Content type
application/json
{
  • "resultInfo": {
    },
  • "data": {
    }
}