<?php
// 引入配置文件
require_once CFG_PATH_CFG . '/apple.cfg.php';
// 引入JWT/JWK工具类文件
require_once dirname(__FILE__) . '/AppleSignin/jwt/JWT.php';
require_once dirname(__FILE__) . '/AppleSignin/jwt/JWK.php';
/**
* 苹果登录相关
*/
class AppleauthLib
{
// 授权登录配置
private $config = array();
// APP内苹果授权登录会提供的参数,由客户端传上来
private $auth = array();
public function __construct()
{
$this->config = cfgGetAppleInfo();
}
/**
* 授权登录信息赋值
* @param $info
*/
public function setAuth($info)
{
$this->auth = array(
'userID' => $info['userID'], // 授权的用户唯一标识
'email' => $info['email'], // 授权的用户资料
'fullName' => $info['fullName'], // 授权的用户资料
'authorizationCode' => $info['authorizationCode'], // 授权code
'identityToken' => $info['identityToken'], // 授权用户的JWT凭证
);
}
/**
* 验证用户是否正常授权,避免攻击
* @param $clientUser
* @param object $payload 对应 $this->decodeIdentityToken() 返回值
* @return bool
*/
public function verifyUser($clientUser, $payload)
{
$sub = (isset($payload->sub)) ? $payload->sub : null;
return $clientUser === $sub;
}
/**
* Decode the Apple encoded JWT using Apple's public key for the signing.
* @return object
* @throws Exception
*/
public function decodeIdentityToken()
{
$publicKeyKid = JWT::getPublicKeyKid($this->auth['identityToken']);
$publicKeyData = $this->fetchPublicKey($publicKeyKid);
$publicKey = $publicKeyData['publicKey'];
$alg = $publicKeyData['alg'];
$payload = JWT::decode($this->auth['identityToken'], $publicKey, array($alg));
return $payload;
}
/**
* Fetch Apple's public key from the auth/keys REST API to use to decode
* the Sign In JWT.
*
* @param string $publicKeyKid
* @return array
* @throws Exception
*/
private function fetchPublicKey($publicKeyKid)
{
$publicKeys = file_get_contents('https://appleid.apple.com/auth/keys');
$decodedPublicKeys = json_decode($publicKeys, true);
if (!isset($decodedPublicKeys['keys']) || count($decodedPublicKeys['keys']) < 1) {
throw new Exception('Invalid key format.');
}
$kids = array_column($decodedPublicKeys['keys'], 'kid');
$parsedKeyData = $decodedPublicKeys['keys'][array_search($publicKeyKid, $kids)];
$parsedPublicKey = JWK::parseKey($parsedKeyData);
$publicKeyDetails = openssl_pkey_get_details($parsedPublicKey);
if (!isset($publicKeyDetails['key'])) {
throw new Exception('Invalid public key details.');
}
return [
'publicKey' => $publicKeyDetails['key'],
'alg' => $parsedKeyData['alg']
];
}
/**
*
* https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
*
* @return mixed
* @throws Exception
*
* 正常返回值示例
* {
* "access_token":"a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw",
* "token_type":"Bearer",
* "expires_in":3600,
* "refresh_token":"r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ",
* "id_token":"eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA"
* }
*/
public function claims()
{
$clientSecret = $this->createClientSecret();
$response = $this->http('https://appleid.apple.com/auth/token', [
'grant_type' => 'authorization_code',
'code' => $this->auth['authorizationCode'], // 授权code是有时效性的,且使用一次即失效
'client_id' => $this->config['app_bundle_id'],
'client_secret' => $clientSecret,
]);
if (!isset($response->access_token)) {
throw new Exception('Error getting an access token: ' . $response->error);
}
$claims = explode('.', $response->id_token)[1];
$claims = json_decode(base64_decode($claims));
return $claims;
}
/**
* 生成 client_secret
* @return string
*/
private function createClientSecret()
{
$ts = time();
$payload = array(
"iss" => $this->config['team_id'],
"aud" => "https://appleid.apple.com", // 默认值
"iat" => $ts,
"sub" => $this->config['app_bundle_id'], // 被授权的APP ID
"exp" => $ts + 86400 * 10
);
$privateKey = $this->config['privateKeyP8'];
$secret = JWT::encode($payload, $privateKey, 'ES256', $this->config['key_id']);
return $secret;
}
private function http($url, $params = false)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($params)
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'User-Agent: curl', # Apple requires a user agent header at the token endpoint
]);
$response = curl_exec($ch);
return json_decode($response);
}
}
测试示例:
<?php
/**
* 苹果授权登录
*
*/
class AppleauthService extends BaseService
{
public function __construct($params = array())
{
parent::__construct($params);
}
/**
* 基于JWT的算法验证
* @param $authInfo
* @return array
*/
public function verifyByJwt($authInfo)
{
$this->lib['appleauth']->setAuth($authInfo);
try {
// 解密
$payload = $this->lib['appleauth']->decodeIdentityToken();
// 验证收权是否正确
$flag = $this->lib['appleauth']->verifyUser($authInfo['userID'], $payload);
!$flag && fnNo(4250, 'no match');
return array(
'userID' => $payload->sub,
'fullName' => isset($authInfo['fullName']) ? $authInfo['fullName'] : '',
'email' => $payload->email,
'datas' => json_encode(objectToArray($payload)),
);
} catch (Exception $e) {
fnWLog(array($e->getMessage()), '', 'apple auth error');
fnNo(4250, 'error', $e->getMessage());
}
return array();
}
/**
* 基于授权码的验证
* @param $authInfo
* @return array
*/
public function verifyByCode($authInfo)
{
$this->lib['appleauth']->setAuth($authInfo);
try {
// 解密
$claims = $this->lib['appleauth']->claims();
return array(
'userID' => $authInfo['userID'],
'fullName' => isset($authInfo['fullName']) ? $authInfo['fullName'] : '',
'email' => isset($authInfo['email']) ? $authInfo['email'] : $claims['email'],
'datas' => json_encode(objectToArray($claims)),
);
} catch (Exception $e) {
fnWLog(array($e->getMessage()), '', 'apple auth error');
fnNo(4250, 'error', $e->getMessage());
}
return array();
}
}