PHP App Store Server API Apple API Refund Query Order History Order PHP Verification Signature Decoding

This article uses thinkPHP to implement related functions, and there are related implementation links on the Internet implemented by Python and java. Don’t talk nonsense, just start.

1. Install jwt.

1. Composer installation [strongly recommended] This article uses this method:

composer require firebase/php-jwt

2. Upload and download the installation package by yourself. The download address is below. If you use this method, please import the relevant load separately, such as [require_once ‘/xxx/xxx/xxx/xxxx/autoload.php’;]:

https://github.com/firebase/php-jwt

Second, find relevant parameters and files

$this->private_key = file_get_contents(‘/xxx/xxxx/xxxx/xxxx/teset.p8’); //Key
//The public key can be downloaded from the official website, but to convert cer to pem PHP can only read the pem format
$this->pub_key = file_get_contents(‘/xxx/xxx/xxx/xxxx/xxx/testappleRootCA-G3.pem’);
$this->kid = ‘test’; //kid
$this->bundle_id = ‘com.test.com.package’; //Project package name
$this->iss = ‘123445-23456-1111-0000-123456789111’;

Apple public key download link, which needs to be converted to pem format: https://www.apple.com/certificateauthority/AppleRootCA-G3.cer

If you don’t know, check out this article, which explains where to find the parameters: WWDC21 – App Store Server API Practice Summary – Tencent Cloud Developer Community – Tencent Cloud

Three, formally write code

<?php

namespace app\admin\controller;
use app\common\controller\Backend;
use think\Db;
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;

/**
 *
 *test
 * @icon fa fa-circle-o
 */
class Testapple extends Backend
{

    private $private_key = null;
    private $header = null;
    private $algorithm = "ES256";
    private $payload = null;
    private $kid = null;
    private $bundle_id=null;
    private $pub_key=null;
    private $iss=null;

    public function _initialize()
    {
        parent::_initialize();
        $this->private_key = file_get_contents('/xxx/xxxx/xxxx/xxxx/teset.p8'); //Key
        //The public key can be downloaded from the official website, but to convert cer to pem PHP can only read the pem format
        $this->pub_key = file_get_contents('/xxx/xxx/xxx/xxxx/xxx/testappleRootCA-G3.pem');
        $this->kid = 'test'; //kid
        $this->bundle_id = 'com.test.com.package'; //Project package name
        $this->iss = '123445-23456-1111-0000-123456789111';
    }

   


    // Request Apple's order query interface
    public function getapple()
    {
        try {
            $orderid = input('orderid');//Receive the input order number query
            $url = "https://api.storekit.itunes.apple.com/inApps/v1/lookup/" . $orderid;
            $res = $this->getres($url);

            if (!empty($res['signedTransactions'])) {
                $data = $res['signedTransactions'];
                $resl = [];
                foreach ($data as $k => $v) {
                    $payload = $this->decodePayNotifyV2($v);
                    $resl[] = $payload;
                }
                dump($resl);
            }
        } catch (\Throwable $th) {
            echo $th->getMessage();
        }
    }

    // Query the historical receipts of Apple users [consumable products only return refund\invalid order data]
    function getthis()
    {
        $orderid = input('orderid');
        $url = 'https://api.storekit.itunes.apple.com/inApps/v1/history/' . $orderid;
        $res = $this->getres($url);
        $resl = [];
        dump($res);
        if (!empty($res['signedTransactions'])) {
            foreach ($res['signedTransactions'] as $k => $v) {
                $payload = $this->decodePayNotifyV2($v);
                $resl[] = $payload;
            }
        }
        dump($resl);
    }

    // Query the order refund record
    function getrefund()
    {
        $orderid = input('orderid');
        $url = 'https://api.storekit.itunes.apple.com/inApps/v1/refund/lookup/' . $orderid;
        $res = $this->getres($url);
        $resl = [];
        dump($res);
        if (!empty($res['signedTransactions'])) {
            foreach ($res['signedTransactions'] as $k => $v) {
                $payload = $this->decodePayNotifyV2($v);
                $resl[] = $payload;
            }
        }
        dump($resl);
    }

  

    //Get Authorization: Bearer
    private function authorbeaer()
    {
         $this->payload = [
            'iss' => $this->iss, //iss value
            'iat' => intval(time()),
            'exp' => intval(time() + 3600),
            'aud' => 'https://appleid.apple.com', // fixed value
            'bid' => $this->bundle_id, // apply bundle_id
        ];

        $this->header = [
            "alg" => "ES256",
            "kid" => $this->kid,
            "typ" => "JWT"
        ];
        $token = JWT::encode($this->payload, $this->private_key, $this->algorithm, $this->kid, $this->header);
        return $token;
    }


    // Verify the signature and decode the returned data
    public function decodePayNotifyV2($jwt)
    {
        list($header, $payload, $sign) = explode('.', $jwt);
        $header_decode = base64_decode($header);
        $header_json = json_decode($header_decode, true);

        if (!isset($header_json['x5c'])) {
            // parsing failed
          
            return false;
        }

        //This step is to convert the public key into the corresponding format
        $pubkey = "-----BEGIN CERTIFICATE-----\\
" . $header_json['x5c'][0] . "\\
-----END CERTIFICATE-- ---";
        try {
            $decoded = JWT::decode($jwt, new Key($pubkey, $header_json['alg']));
            // Determine whether the returned certificate chain is equal to the Apple public key certificate
            $pem = $this->pub_key;
            $str = str_replace("\r\\
", "", $pem);
            $pubPem = "-----BEGIN CERTIFICATE-----" . $header_json['x5c'][2] . "-----END CERTIFICATE-----";
            if ($str != $pubPem) {
                // The returned public key certificate chain error
               
            }
        } catch (\Exception $e) {
            // signature verification failed
            
            return false;
        }
        return json_decode(json_encode($decoded), true);
    }

    //Initiate a request to get token
    private function getres($url, $token = null)
    {
        $jwt_str = $token  $this->authorbeaer();
        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_HTTPHEADER => array(
                'Authorization: Bearer' . $jwt_str
            ),
        ));
        $response = curl_exec($curl);
        $errno = curl_errno($curl);
        if (!empty($errno)) {
            echo 'err:' . $errno;
        }
        curl_close($curl);
        $res = json_decode($response, true);
        return $res;
    }
}

3. Summary

The code should be quite simple. The general process is: convert the following three parts into JWT and put it in the Authorization in the header of the get request. The form is: Bearer AJDasdfaSFJFJFGFL【’JWT’】,

  • header: mainly declares the signature algorithm of JWT;
  • payload: mainly carries various declarations and transmits plaintext data;
  • signature: A JWT with this part is called a JWS, which is a signed JWS

Then if it succeeds, it will return the corresponding result, here are some pitfalls:

1. When querying the order number, you need to use the order number of the client. The general form is like this: MVL678M6AL; if you use other order numbers to query, it will return: {status:1}, which is an order error prompt;

2. When verifying and returning the result, pay attention to the parameters of the decode function. If the returned result is as shown in the figure below,

$jwt: is the [signedTransactions[0] value], $header_json[‘alg’] is the one before transcoding, here is ES256; $pubkey here is not the public key downloaded from the official website, After the jwt is divided by a decimal point (.), base64_decode and then json_decode to get a key: [x5c] array, which contains 3 certificate chains, and the certificate chain with subscript 0 is used as the value here. The public key prepared at the beginning is used to compare the certificate chain with the X5C subscript 2

JWT::decode($jwt, new Key($pubkey, $header_json['alg']));

As for the others, you are welcome to add, if you have any questions, you can leave a message in the comment area, and everyone can discuss.

Below is an article written by Daniel, take a look at it in detail, it is very well written:

WWDC21 – App Store Server API Practice Summary – Tencent Cloud Developer Community – Tencent Cloud

WWDC21 – App Store Server API Practice Summary – Nuggets

The verification of the big guy below is a bit of a problem, for reference only

Apple store refund API PHP signature verification signedPayload – Programmer Sought