Apple登录“ invalid_client”,使用PHP和openSSL对JWT进行身份验证

我正在尝试使用此library将Apple登录到Android应用中。文档中描述了主要流程:该库在Android端返回授权代码。此授权码必须发送到我的后端,然后,我的后端又将其发送到Apple服务器,以便取回访问令牌。

herehere所述,为了获取访问令牌,我们需要向Apple API发送参数列表,授权代码和签名的JWT。特别是,JWT需要使用ES256算法使用.p8私钥进行签名,该私钥必须从Apple开发人员门户生成和下载。 Apple doc

这是我的PHP脚本:

<?php

$authorization_code = $_POST('auth_code');

$privateKey = <<<EOD
-----BEGIN PRIVATE KEY-----
my_private_key_downloaded_from_apple_developer_portal (.p8 format)
-----END PRIVATE KEY-----
EOD;

$kid = 'key_id_of_the_private_key'; //Generated in Apple developer Portal
$iss = 'team_id_of_my_developer_profile';
$client_id = 'identifier_setted_in_developer_portal'; //Generated in Apple developer Portal

$signed_jwt = $this->generateJWT($kid,$iss,$client_id,$privateKey);

$data = [
            'client_id' => $client_id,'client_secret' => $signed_jwt,'code' => $authorization_code,'grant_type' => 'authorization_code'
        ];
$ch = curl_init();

curl_setopt($ch,CURLOPT_URL,'https://appleid.apple.com/auth/token');
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_POSTFIELDS,$data);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);

$serverOutput = curl_exec($ch);

curl_close ($ch);

var_dump($serverOutput);

function generateJWT($kid,$sub,$key) {

    $header = [
        'alg' => 'ES256','kid' => $kid
    ];
    $body = [
        'iss' => $iss,'iat' => time(),'exp' => time() + 3600,'aud' => 'https://appleid.apple.com','sub' => $sub
    ];

    $privKey = openssl_pkey_get_private($key);
    if (!$privKey) return false;

    $payload = $this->encode(json_encode($header)).'.'.$this->encode(json_encode($body));
    $signature = '';
    $success = openssl_sign($payload,$signature,$privKey,OPENSSL_ALGO_SHA256);
    if (!$success) return false;

    return $payload.'.'.$this->encode($signature);
}

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

?>

问题在于Apple的回应总是:

{"error":"invalid_client"}

阅读here似乎问题可能与openSSL有关,后者生成的签名对Apple不正确(“ OpenSSL的ES256签名结果是DER编码的Asn.1结构(其大小超过64)。(不是原始R || S值)“)。

是否可以使用openSSL获得正确的签名?

p8格式是openssl_sign和openssl_pkey_get_private函数的正确输入吗? (我注意到提供的.p8密钥如果在jwt.io中用于计算签名的jwt,则无法使用。)

在openSSL文档中,我读到应该提供一个pem密钥,如何在.pem密钥中转换.p8?

我还尝试了一些PHP库,它们基本上使用了与上述相同的步骤,例如firebase/php-jwtlcobucci/jwt,但是Apple的响应仍然是“无效的客户端”。

预先感谢您的帮助,

编辑

我试图从等式中完全删除openSSL。使用从.p8生成的.pem密钥,我用jwt.io生成了一个签名的JWT。使用此签名的JWT,Apple API可以正确答复。至此,我几乎可以确定这是一个openSSL签名问题。关键问题是如何使用PHP和openSSL获得正确的ES256签名。

drx321 回答:Apple登录“ invalid_client”,使用PHP和openSSL对JWT进行身份验证

here所示,问题实际上出在openSSL生成的签名中。

使用ES256,数字签名是两个无符号整数的串联,分别表示为R和S,这是椭圆曲线(EC)算法的结果。 R的长度|| S是64。

openssl_sign函数生成一个签名,该签名是DER编码的ASN.1结构(大小> 64)。

解决方案是将DER编码的签名转换为R和S值的原始串联。在this library中,存在执行此转换的函数“ fromDER ”:

    /**
     * @param string $der
     * @param int    $partLength
     *
     * @return string
     */
    public static function fromDER(string $der,int $partLength)
    {
        $hex = unpack('H*',$der)[1];
        if ('30' !== mb_substr($hex,2,'8bit')) { // SEQUENCE
            throw new \RuntimeException();
        }
        if ('81' === mb_substr($hex,'8bit')) { // LENGTH > 128
            $hex = mb_substr($hex,6,null,'8bit');
        } else {
            $hex = mb_substr($hex,4,'8bit');
        }
        if ('02' !== mb_substr($hex,'8bit')) { // INTEGER
            throw new \RuntimeException();
        }
        $Rl = hexdec(mb_substr($hex,'8bit'));
        $R = self::retrievePositiveInteger(mb_substr($hex,$Rl * 2,'8bit'));
        $R = str_pad($R,$partLength,'0',STR_PAD_LEFT);
        $hex = mb_substr($hex,4 + $Rl * 2,'8bit');
        if ('02' !== mb_substr($hex,'8bit')) { // INTEGER
            throw new \RuntimeException();
        }
        $Sl = hexdec(mb_substr($hex,'8bit'));
        $S = self::retrievePositiveInteger(mb_substr($hex,$Sl * 2,'8bit'));
        $S = str_pad($S,STR_PAD_LEFT);
        return pack('H*',$R.$S);
    }
    /**
     * @param string $data
     *
     * @return string
     */
    private static function preparePositiveInteger(string $data)
    {
        if (mb_substr($data,'8bit') > '7f') {
            return '00'.$data;
        }
        while ('00' === mb_substr($data,'8bit') && mb_substr($data,'8bit') <= '7f') {
            $data = mb_substr($data,'8bit');
        }
        return $data;
    }
    /**
     * @param string $data
     *
     * @return string
     */
    private static function retrievePositiveInteger(string $data)
    {
        while ('00' === mb_substr($data,'8bit') > '7f') {
            $data = mb_substr($data,'8bit');
        }
        return $data;
    }

另一点是,应将.pem密钥提供给open_ssl_sign函数。从从Apple开发人员下载的.p8密钥开始,我已经使用openSSL创建了.pem:

openssl pkcs8 -in AuthKey_KEY_ID.p8 -nocrypt -out AuthKey_KEY_ID.pem

以下是我的新 generateJWT 函数代码,该代码使用.pem密钥和fromDER函数来转换由openSSL生成的签名:

    function generateJWT($kid,$iss,$sub) {
        
        $header = [
            'alg' => 'ES256','kid' => $kid
        ];
        $body = [
            'iss' => $iss,'iat' => time(),'exp' => time() + 3600,'aud' => 'https://appleid.apple.com','sub' => $sub
        ];

        $privKey = openssl_pkey_get_private(file_get_contents('AuthKey_.pem'));
        if (!$privKey){
           return false;
        }

        $payload = $this->encode(json_encode($header)).'.'.$this->encode(json_encode($body));
        
        $signature = '';
        $success = openssl_sign($payload,$signature,$privKey,OPENSSL_ALGO_SHA256);
        if (!$success) return false;

        $raw_signature = $this->fromDER($signature,64);
        
        return $payload.'.'.$this->encode($raw_signature);
    }

希望有帮助

本文链接:https://www.f2er.com/2770398.html

大家都在问