签名验证失败 - 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】:
这里是使用 html、javascript 和 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,您可以提取您需要的字段,例如 userId
和 email
,即 $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->getPublicKey()
是什么。你可能在那里做各种错误的事情哈哈
来自appleid.apple.com/auth/token
的 Apple 响应是否有效?我们应该将此移至聊天 [chat.***.com/rooms/219913/…
我无法使用那个聊天室。我已经编辑了我的问题以上是关于签名验证失败 - Apple 使用 Firebase JWT 登录的主要内容,如果未能解决你的问题,请参考以下文章