JWTを作成してGoogleAPIを叩く


目的

PHPでJWTを作成して、「Google Play Developer API」を叩こうじゃないか

前提

本記事では上記Google Play Developer APIのv2が2019/12/1に完全廃止になる際に、
v3への移行を行ったのですが、その際に本当にv2止まってんのか?
ということを検証した際に得た知見を紹介します。

ちなみに今回利用するAPIは
Google Play Developer API
というAPIですが、このAPIを利用することでAndroidアプリ内の以下のような情報を参照できたりします。

  • 製品の購入状況
  • サブスクリプション購入ステータス

今回はそんな上記APIを手元で検証するためにJWTトークンを作成し、実際にAPIを叩きます。

注)
そもそもJWTなんて手動で作る必要性はなく各言語ごとにライブラリが展開されています。
本記事は様々な事情があったため自らJWTを作成し、Google Play Developer APIのv2が廃止になっているのか試すために行った手法です。

Google Play Developer APIを利用に当たって

以下二つのどちらかの認証済みクライアントを用意する必要がありますが、
本稿ではサーバ側での課金検証のため後者のサービスアカウントを利用した方法となります。

  • OAuth クライアント
  • サービス アカウント

JWT とは

こちら参考にさせていただきました。
https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6

JWT作成に必要な情報

  • Developer Consoleで作成したclientEメールアドレス(クライアントIDの部分に書いてある)
  • 秘密鍵(Developer Console からダウンロードできる)

作成手順概要

サービスアカウントを利用してAPIを利用する場合には以下の図のような流れになります。
簡単なイメージは以下の通りです。
1.JWTを作る
2.作ったJWTをGoogleのサーバに投げ、APIのアクセストークンを取得
3.取得したトークンでAPIを叩く

1.JWTを作る

まず1.JWT作る
からやっていきますが
JWTのフォーマットはドキュメントにもある通り以下のようなフォーマットとなっています。

{Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}

訳) {base64エンコードしたheader}.{base64エンコードしたclaims}.{base64エンコードした署名}

ますはheaderですが、公式に記載ある通りなので、一旦受け止めます。
Forming the JWT header

{"alg":"RS256","typ":"JWT"}

次にclameです。
これも公式にあるので受け止めます。
Forming the JWT claim set

仕様については主に以下記載です。

Name Description
iss The email address of the service account.
scope A space-delimited list of the permissions that the application requests.
aud A descriptor of the intended target of the assertion. When making an access token request this value is always https://oauth2.googleapis.com/token.
exp The expiration time of the assertion, specified as seconds since 00:00:00 UTC, January 1, 1970. This value has a maximum of 1 hour after the issued time.
iat The time the assertion was issued, specified as seconds since 00:00:00 UTC, January 1, 1970.
{
  "iss":"[email protected]",
  "scope":"https://www.googleapis.com/auth/devstorage.readonly",
  "aud":"https://oauth2.googleapis.com/token",
  "exp":1328554385,
  "iat":1328550785
}

iss: Developer Consoleで作ったクライアントIDの部分に書いてあるメールアドレス。 
     xxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.comの形 
scope: 利用するAPIスコープ 
aud: クライアント識別子。アクセストークンを発行する場合はhttps://www.googleapis.com/oauth2/v3/token
exp: トークンの有効期限。最大でiatから1時間分まで 
iat: トークンの発行日 (1970年1月1日00:00:00 UTCからの秒数で指定)

不親切なのはscopeの部分です。

A space-delimited list of the permissions that the application requests.
訳)アプリケーションが要求する権限のスペース区切りリスト。

それでどこにあるの?そのリスト?って思いませんか?
ググったら簡単に見つかりますがリンクくらい貼ってくれてもいいですよね。
scope一覧はここにあります。
https://developers.google.com/identity/protocols/googlescopes

今回使いたいのは
アプリ内課金のレシート検証を行いたいので以下がscopeとなります。

https://www.googleapis.com/auth/androidpublisher

上記踏まえ以下のようなコードでJWT取得できます。

        $jwt_now = time();
        $subscription_configs = $this->config->item('android');
        $auth_config = $subscription_configs['auth_config'];

        $header = array(
            "alg" =>  "RS256",
            "typ" => "JWT"
        );
        $payload = array(
            "iss" => "[email protected]",
            "scope" => 'https://www.googleapis.com/auth/androidpublisher',
            "aud" => 'https://oauth2.googleapis.com/token',
            'iat' => $jwt_now,
            'exp' => $jwt_now + 3600,
        );

        //Developer Console からダウンロードした秘密鍵
        $private_key = $auth_config['private_key'];

        $header_json = Json::encode($header);
        $payload_json = Json::encode($payload);

        $header_b64 = UrlSafeBase64::encode($header_json);
        $payload_b64 = UrlSafeBase64::encode($payload_json);
        $message = $header_b64 . '.' . $payload_b64;

        $signature = '';
        $success = openssl_sign($message, $signature, $private_key, "SHA256");
        $signature_b64 = UrlSafeBase64::encode($signature);
        $jwt = $message . '.' . $signature_b64;

このコードで気になる部分は以下コード6割部分くらいにある
UrlSafeBase64::encodeopen_sslsignの部分だと思いますので、簡単に記載します。

まずUrlSafeBase64::encode()の方ですが
公式にあったコードを引用しました。
UrlSafeBase64::encode()の中身はこれと同様の内容を行っているものとしてください。
こんな感じのコードで、base64エンコードしたものをURLセーフにエンコードしています。

function base64url_encode($data) {
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

base64エンコードとbase64URLエンコードの違いは本記事では触れません。

次にopen_sslsignですが公式に記載ある通り署名が作れる関数らしいです。

指定した data の署名を作ります。 priv_key_id で指定した秘密鍵を使用して、暗号のデジタル署名を生成します。 data 自体は暗号化されないことに注意してください。

と、上記ソースでようやくJWTが取得できます。
あとは、この取得したJWTとCurl使って検証を行っていくという流れです。
変わったことはしていませんが簡単に記載します。
2.作ったJWTをGoogleのサーバに投げ、APIのアクセストークンを取得
3.取得したトークンでAPIを叩く

2.作ったJWTをGoogleのサーバに投げ、APIのアクセストークンを取得

Making the access token request

$curl -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer -d assertion={1.JWTを作る で作成したJWT} https://www.googleapis.com/oauth2/v4/token

3.取得したトークンでAPIを叩く

今回利用したいのはGoogleAndroidPublusherAPIなのでcurlを利用した場合以下のようなコマンドになります。
Purchases.subscriptions: get

$curl https://www.googleapis.com/androidpublisher/v3/applications/{package_name}/purchases/subscriptions/{subscription_id(課金Item)}/tokens/{3.で取得したアクセストークン}

備考

ここのリクエストURLに含めまれているv3をv2に変更するとGoogleAndroidPublusherAPIのv2へのリクエストとなります。
ちなみにv2は12/3時点ではまだ生きてました。

学び

斜め読みするとドキュメント内の大事な部分のニュアンスを掴み損ねがちだと思いました。
英語だと特に。

参考

[認証におけるJWTの利用について]
https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6
[Google公式]
https://developer.android.com/google/play/developer-api
https://developers.google.com/identity/protocols/OAuth2ServiceAccount