Apple Sign In “invalid_client”, signing JWT for authentication using PHP and openSSL
我正在尝试使用此库将Apple登录到Android应用。文档中描述了主要流程:该库在Android端返回授权代码。此授权码必须发送到我的后端,后者再将其发送到Apple服务器,以便取回访问令牌。
如此处和此处所述,为了获取访问令牌,我们需要向Apple API发送参数列表,授权代码和已签名的JWT。特别是,JWT需要使用ES256算法使用.p8私钥进行签名,该私钥必须从Apple开发人员门户生成和下载。苹果doc
这是我的PHP脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <?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, $iss, $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, '='); } ?> |
问题在于苹果的回应始终是:
1 | {"error":"invalid_client"} |
在这里阅读似乎问题可能与openSSL有关,后者生成的签名对Apple不正确(" OpenSSL的ES256签名结果是DER编码的ASN.1结构(其大小超过64))。(不是原始R || S值)")。
是否有一种方法可以使用openSSL获得正确的签名?
p8格式是openssl_sign和openssl_pkey_get_private函数的正确输入吗?
(我注意到,如果在jwt.io中使用了所提供的.p8密钥以计算带符号的jwt,则该密钥将不起作用。)
在openSSL文档中,我读到应提供一个pem密钥,如何将.p8转换为.pem密钥?
我还尝试了一些PHP库,这些库基本上使用与上述相同的步骤,例如firebase / php-jwt和lcobucci / jwt,但是Apple响应仍然是"无效的客户端"。
在此先感谢您的帮助,
编辑
我试图从等式中完全删除openSSL。使用从.p8生成的.pem密钥,我用jwt.io生成了一个签名的JWT。使用此签名的JWT,Apple API可以正确答复。至此,我几乎可以确定这是一个openSSL签名问题。关键问题是如何使用PHP和openSSL获得正确的ES256签名。
如此处所示,问题实际上出在openSSL生成的签名中。
使用ES256,数字签名是两个无符号整数的串联,分别表示为R和S,这是椭圆曲线(EC)算法的结果。 R的长度|| S为64.
openssl_sign函数生成一个签名,该签名是DER编码的ASN.1结构(大小> 64)。
解决方案是将DER编码的签名转换为R和S值的原始串联。在此库中,存在执行此转换的函数" fromDER":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /** * @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, 0, 2, '8bit')) { // SEQUENCE throw new \ untimeException(); } if ('81' === mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128 $hex = mb_substr($hex, 6, null, '8bit'); } else { $hex = mb_substr($hex, 4, null, '8bit'); } if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new \ untimeException(); } $Rl = hexdec(mb_substr($hex, 2, 2, '8bit')); $R = self::retrievePositiveInteger(mb_substr($hex, 4, $Rl * 2, '8bit')); $R = str_pad($R, $partLength, '0', STR_PAD_LEFT); $hex = mb_substr($hex, 4 + $Rl * 2, null, '8bit'); if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new \ untimeException(); } $Sl = hexdec(mb_substr($hex, 2, 2, '8bit')); $S = self::retrievePositiveInteger(mb_substr($hex, 4, $Sl * 2, '8bit')); $S = str_pad($S, $partLength, '0', STR_PAD_LEFT); return pack('H*', $R.$S); } /** * @param string $data * * @return string */ private static function preparePositiveInteger(string $data) { if (mb_substr($data, 0, 2, '8bit') > '7f') { return '00'.$data; } while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') <= '7f') { $data = mb_substr($data, 2, null, '8bit'); } return $data; } /** * @param string $data * * @return string */ private static function retrievePositiveInteger(string $data) { while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f') { $data = mb_substr($data, 2, null, '8bit'); } return $data; } |
另一点是,应将.pem密钥提供给open_ssl_sign函数。从从Apple开发人员下载的.p8密钥开始,我已经使用openSSL创建了.pem:
1 | openssl pkcs8 -in AuthKey_KEY_ID.p8 -nocrypt -out AuthKey_KEY_ID.pem |
以下是我的新generateJWT函数代码,该代码使用.pem键和fromDER函数转换由openSSL生成的签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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); } |
希望对您有所帮助