签名验证失败 - Apple 使用 Firebase JWT 登录

Posted

技术标签:

【中文标题】签名验证失败 - Apple 使用 Firebase JWT 登录【英文标题】:Signature Verification Failed - Apple Signin Using Firebase JWT 【发布时间】:2020-12-04 05:42:49 【问题描述】:

我正在尝试生成客户端密码并在 php 中使用 Firebase/php-jwt 验证它以获取苹果符号。

      // generate the client secret
      payload = array(
          "iss" => $teamId,
          'aud' => 'https://appleid.apple.com',
          'iat' => time(),
          'exp' => time() + 3600,
          'sub' => $clientId
      );
      $keycontent = file_get_contents($uri);
      
      $jwt = JWT::encode($payload, $keycontent, 'ES256', $key);

      //Decode the jwt token 
      $decoded = JWT::decode($jwt, $rsa->getPublicKey(), array('ES256'));

从苹果获取公钥 (https://appleid.apple.com/auth/keys)

我在执行代码时收到签名验证失败。

这就是我获取苹果公钥的方式

  $cURLConnection = curl_init();

  curl_setopt($cURLConnection, CURLOPT_URL, 'https://appleid.apple.com/auth/keys');
  curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

  $publickeys = curl_exec($cURLConnection);
  curl_close($cURLConnection);

  $jsonArrayResponse = json_decode($publickeys);

  foreach ($jsonArrayResponse->keys as $publicKey => $publicValue) 
    if ($publicValue->kid == $d_keys->kid) 
      $rsa = new RSA();
      $rsa->loadKey([
        'e' => new BigInteger(base64_decode($publicValue->e), 256),
        'n' => new BigInteger(base64_decode($publicValue->n), 256)
      ]);
      $decoded = JWT::decode($clientSecretToken, $rsa->getPublicKey(), array('ES256'));
    
   

【问题讨论】:

澄清一下,您的 $keycontent 是您在 Apple Dev Portal 上创建的密钥,而您的 $key 是与该密钥关联的密钥 ID? @Merricat yes $keycontent -> 苹果提供的私钥和$key -> 密钥ID 【参考方案1】:

这里是使用 htmljavascript 和 PHP 登录 Apple 的完整示例。

我使用来自https://github.com/firebase/php-jwt的jQuery和PHP-JWT

首先从 Apple 开发者门户创建您的 ID 和密钥。这些资源将帮助您获得 https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple https://sarunw.com/posts/sign-in-with-apple-4/

登录分两个阶段进行,首先客户端单击“Sign In With Apple”按钮并通过 Apple 进行身份验证。这会将两条信息返回到我们的 Javascript,然后我们可以将它们发布到 Apple 的服务器以验证客户端并使用 PHP 获取他们的信息。

在本例中,我们使用 Javascript/PHP 来处理登录过程。 Apple 的响应是使用 Javascript/PHP 处理的,而不是通过重定向 URL。永远不会调用重定向 URL。

HTML/JS 客户端:

<div id="appleid-signin"  data-color="white" data-border="true" data-type="sign in" data- data- style="margin-top: 18px; cursor: pointer;"></div>
     <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
     <script type="text/javascript">
     jQuery(document).ready(function()

        AppleID.auth.init(
            clientId : "YOUR.CLIENT.ID",
            scope : "name email",
            redirectURI : "YOUR://REDIRECT/URI",
            usePopup : true
        );
 );
    
    
    document.addEventListener("AppleIDSignInOnSuccess", (data) => 
        //handle successful response
        
        console.log(data);
        
        var appleToken = data.detail.authorization.id_token ;
        var appleCode = data.detail.authorization.code ;
        console.log("Token: "+appleToken);
        console.log("Code: "+appleCode);

        jQuery.ajax(url: "verifyToken.php?authCode="+appleCode+"&idToken="+appleToken, success: function(result)
                var appleUser = JSON.parse(result);
                console.log(appleUser);
                console.log("Customer Email: " + appleUser.email);
        );
    );
    
    </script>

以上代码由 HTML Div 组成,其中包含 Apple 登录按钮和 Apple 托管的 Apple javascript 登录。

我使用jQuery在页面加载后调用AppleID.auth.init函数,以确保在我们尝试调用其函数之前加载Apple托管的JS。

用户成功通过 Apple 身份验证后,来自 Apple 的响应会被处理,我们会将其发布到我们的 PHP 脚本中,以向 Apple 验证信息并检索客户信息。 PHP 从 Apple 返回客户信息,在本例中,它将这些信息写入 Web 浏览器控制台,后跟客户电子邮件地址。

这是处理这个的 PHP (verifyToken.php)。替换顶部的变量并上传您的私钥(最好在安全的地方)。我已经添加了关于在哪里可以找到可用信息的描述:

<?php

// Requires https://github.com/firebase/php-jwt
// Install with: composer require firebase/php-jwt

$id_token = $_REQUEST['idToken']; // Provided after user completed sign in. In authorisation->id_token
$client_authorization_code = $_REQUEST['authCode']; // Provided after user completed sign in. In authorisation->code
$teamId = "01ABC23D4E" ; // Your Team ID from https://developer.apple.com/account/#/membership/
$clientId = "YOUR.CLIENT.ID" ; // Your sing in with apple identifier from https://developer.apple.com/account/resources/identifiers/list
$privKey = file_get_contents("AppleSignIn_AuthKey.p8"); // Provided by Apple only once after you generate a key at https://developer.apple.com/account/resources/authkeys/list
$keyID = "1A2BCD3EFG" ; // The ID for your key from https://developer.apple.com/account/resources/authkeys/list

require __DIR__ . '/vendor/autoload.php';
use \Firebase\JWT\JWT;
use \Firebase\JWT\JWK;  

$apple_jwk_keys = json_decode(file_get_contents("https://appleid.apple.com/auth/keys"), null, 512, JSON_OBJECT_AS_ARRAY) ;
$keys = array() ;
foreach($apple_jwk_keys->keys as $key)
    $keys[] = (array)$key ;
$jwks = ['keys' => $keys];

$header_base_64 = explode('.', $id_token)[0];
$kid = JWT::jsonDecode(JWT::urlsafeB64Decode($header_base_64));
$kid = $kid->kid;

$public_key = JWK::parseKeySet($jwks);
$public_key = $public_key[$kid]; 

$payload = array(
 "iss" => $teamId,
 'aud' => 'https://appleid.apple.com',
 'iat' => time(),
 'exp' => time() + 3600,
 'sub' => $clientId
);

$client_secret = JWT::encode($payload, $privKey, 'ES256', $keyID);

$post_data = [
  'client_id' => $clientId,
  'grant_type' => 'authorization_code',
  'code' => $client_authorization_code,
  'client_secret' => $client_secret
];

$ch = curl_init("https://appleid.apple.com/auth/token");
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
   'Accept: application/x-www-form-urlencoded',
   'User-Agent: curl',  //Apple requires a user agent header at the token endpoint
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$curl_response = curl_exec($ch);
curl_close($ch);

$data = json_decode($curl_response, true);
$refresh_token = $data['refresh_token'];

$claims = explode('.', $data['id_token'])[1];
$claims = json_decode(base64_decode($claims));

echo json_encode($claims);

本 PHP 使用之前 Javascript 中从 Apple 返回的信息与 Apple 验证信息。它将来自 Apple 的信息返回给 Javascript。

它返回的信息如下:

(
    [iss] => https://appleid.apple.com
    [aud] => YOUR.CLIENT.ID
    [exp] => 1614170648
    [iat] => 1614084248
    [sub] => XXXXX.XXXXX.XXXXX
    [at_hash] => XXXXXX
    [email] => customers@email.address
    [email_verified] => true
    [auth_time] => 1614084210
    [nonce_supported] => 1
)

该过程已完成,请根据需要使用此信息来创建/登录用户。

如果您在 ios/macOS 上使用 Apple 登录,那么您可以使用“sub”来查找用户,因为这与 this 返回的相同:

    ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
    NSString *user = appleIDCredential.user;

【讨论】:

这很好。一个问题:从 Apple 获取密钥以构建公钥以获取从未使用过的 parsed_id_token 的部分可以跳过吗? @Mirko 好地方!可以跳过它,我已将其从示例中删除。【参考方案2】:

问题在于您混淆了登录流程的各个部分。您将自己的 client_secret 创建与 Apple 的 id_token 验证混淆了。你想做的是这样的:

    从客户端应用程序接收 Apple 的 id_token (JWT) 和 authorization_code 解码id_token的标头,以便您可以获取kid(用于验证签名)
$header_base_64 = explode('.', $id_token)[0];
$kid = (JWT::jsonDecode(JWT::urlsafeB64Decode($header_base_64)))->kid;
    使用 Apple 的公钥 (GET https://appleid.apple.com/auth/keys) 和 RS256 算法验证 id_token 的签名。这些是 JWK 格式,因此您需要自己构建密钥,使用您刚刚从 id_token 中提取的 kid
$public_key = (JWK::parseKeySet($apple_jwk_keys))[$kid]; 
$parsed_id_token = JWT::decode($id_token, $public_key, ['RS256']);

    如果一切顺利,您现在知道您的用户向您发送了一个有效的 Apple id_token,您可以提取您需要的字段,例如 userIdemail,即 $user_id = $parsed_id_token['sub']

    下一步是将您的authorization_code 换成refresh_token,这样您就可以每天最多验证一次用户。首先创建你的client_secret,一个包含你已经创建的所有字段的 JWT。然后,您使用自己的 Key + KeyID(在 Apple 开发门户上创建)对此进行签名,这次使用 ES256 算法。代码与您已有的相同:

payload = array(
 "iss" => $teamId,
 'aud' => 'https://appleid.apple.com',
 'iat' => time(),
 'exp' => time() + 3600,
 'sub' => $clientId
);

$keycontent = file_get_contents($uri);      
$client_secret = JWT::encode($payload, $keycontent, 'ES256', $key);
    现在您将 authorization_code 发送给 Apple。 (请注意,如果您的 id_token 是由 iOS 应用程序生成的,那么您的 client_id 是您的应用程序标识符。如果它来自 Web 客户端,那么您需要创建一个专用的服务 ID
//1. build POST data
$post_data = [
  'client_id' => $clientId,
  'grant_type' => 'authorization_code',
  'code' => $client_authorization_code,
  'client_secret' => $client_secret
];

//2. create and send request
$ch = curl_init("https://appleid.apple.com/auth/token");
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
   'Accept: application/x-www-form-urlencoded',
   'User-Agent: curl',  //Apple requires a user agent header at the token endpoint
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$curl_response = curl_exec($ch);
curl_close($ch);

//3. extract JSON from Apple token response
$data = json_decode($curl_response, true);
$refresh_token = $data['refresh_token'];

现在您可以将这个refresh_token 保存在您的数据库中,用于这个特定的userId。这样,您最多可以每 24 小时验证一次用户的真实性。您所要做的就是使用refresh_token 而不是authorization_code 重复第6 步,同时更改grant_type(请记住,这次Apple 不会给您一个新的refresh_token)。

就是这样!您无需验证自己的签名client_secret,它是您创建的!苹果是需要这样做的,让他们来处理。

【讨论】:

我之前使用的是 .p8 格式,然后更改为 .pem 格式。仍然面临同样的问题。当我尝试在 jwt.io 中验证 JWT 时,它总是抛出“无效签名” 是的,我收到了 [appleid.apple.com/auth/token] 的回复。在此之后,我使用 [appleid.apple.com/auth/keys] 获取公钥。签名验证问题即将上线 $decoded = JWT::decode($clientSecretToken, $rsa->getPublicKey(), array('ES256')); 我的猜测是您的 JWT 没问题,您在使用 Apple 的公钥验证其签名时遇到了问题。我几乎可以肯定您没有正确创建它们,这就是为什么我建议直接尝试向 Apple 的 /auth/token 端点发送 POST 请求。例如,您没有解释$rsa-&gt;getPublicKey() 是什么。你可能在那里做各种错误的事情哈哈 来自appleid.apple.com/auth/token 的 Apple 响应是否有效?我们应该将此移至聊天 [chat.***.com/rooms/219913/… 我无法使用那个聊天室。我已经编辑了我的问题

以上是关于签名验证失败 - Apple 使用 Firebase JWT 登录的主要内容,如果未能解决你的问题,请参考以下文章

应用程序失败的协同设计验证烦人的错误!

IOS 验证/提交到 iTunes Connect 失败

验证 JWT 令牌签名

ES256 JWT 在 PHP 中为 Apple AppStoreConenct API 身份验证签名

我可以存储 Apple 的公钥来验证令牌签名吗?

签名验证失败。没有提供安全密钥来验证签名