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

Signature Verification Failed - Apple Signin Using Firebase JWT

我正在尝试生成客户端密码并使用 php 中的 Firebase/php-jwt 进行验证以获取 apple sign。

      // 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'));

正在从苹果中获取 public 密钥 (https://appleid.apple.com/auth/keys)

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

这就是我获得苹果 public 钥匙的方式

  $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'));
    }
   }

问题是您混淆了 sign-in 流程的一部分。您将自己的 client_secret 创作与 Apple 的 id_token 验证混淆了。你想要做的是:

  1. 从客户端应用程序接收 Apple 的 id_token (JWT) 和 authorization_code
  2. 解码id_token的header,这样你就可以获取kid(用于验证签名)
$header_base_64 = explode('.', $id_token)[0];
$kid = (JWT::jsonDecode(JWT::urlsafeB64Decode($header_base_64)))->kid;
  1. 使用 Apple 的 public 密钥 (GET https://appleid.apple.com/auth/keys) 和 RS256 算法验证 id_token 的签名。这些是 JWK 格式,因此您需要使用刚刚从 id_token
  2. 中提取的 kid 自己构建密钥
$public_key = (JWK::parseKeySet($apple_jwk_keys))[$kid]; 
$parsed_id_token = JWT::decode($id_token, $public_key, ['RS256']);
  1. 如果一切顺利,您现在知道您的用户向您发送了一个有效的 Apple id_token,您可以提取所需的字段,例如 userIdemail,即 $user_id = $parsed_id_token['sub']

  2. 下一步是将您的 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);
  1. 现在您将 authorization_code 发送给 Apple。 (请注意,如果您的 id_token 是由 iOS 应用程序生成的,则您的 client_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 的签名,您创建了它! Apple 是需要这样做的人,让他们来处理吧。

这是一个完整的示例,通过 HTML、Javascript 和 PHP 使用 Sign In With Apple。

我使用 jQuery 和 https://github.com/firebase/php-jwt

中的 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/

登录分两个阶段进行,首先客户端单击“使用 Apple 登录”按钮并向 Apple 进行身份验证。这 returns 两条信息到我们的 Javascript 然后我们可以 post 到 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-height="40" data-width="200" 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 的响应,我们 post 将此关闭到我们的 PHP 脚本以向 Apple 验证信息并检索客户信息。 PHP returns 来自 Apple 的客户信息,在此示例中,它将此信息写入 Web 浏览器控制台,后跟客户电子邮件地址。

这是处理此 (verifyToken.php) 的 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的信息。它returns从Apple返回的信息Javascript。

它returns的信息如下:

(
    [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
)

该过程已完成,请根据需要向 create/login 用户使用此信息。

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

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