最近終於研究出如何使用 Sign in with Apple 包含前後端的串接,分享給大家。
(本文同步於 iPlayground 2020 演講)
投影片:
iPlayground 影音版:https://www.youtube.com/watch?v=DwXhZwRDlzk
為什麼要串?
在 Apple 開發者大會 WWDC 2019 發表了 Sign in with Apple 的功能,iOS 13 後皆支援。
主要二個重點:
- 使用 Apple ID 登入,強化保護您的隱私權
- 當用戶不願給出自己的 E-mail 時,蘋果可以生成一組
「虛擬 E-mail」給用戶使用,
「虛擬 E-mail」收到的資訊,蘋果會代轉給用戶的「真實 E-mail」
官方消息指出,從 2019 年 9 月 12 日開始,新上架的 APP 需設置 Apple ID 登入;
在 2020 年 4 月之後,蘋果直接強制要求,
只要您的 app 有支援第三方登入 (例如:Facebook 登入、Google 登入、Twitter 登入…等)
就「一定要」支援 Sign in with Apple (硬要多一顆按鈕),相當於強迫中獎的概念。
基本上只要有帳號系統的服務網站,幾乎全部得加。
否則會怎麼樣呢? app 送審不會過,新 app 無法上架,新的 app 更新無法推上架提供更新。
串接 Sign in with Apple 勢在必行,因為 App Store 是 Apple 在管,官方擁有至高無上的權力。
Apple Sign In 的原理
Apple Sign In 主要接近於標準的 OAuth 流程,我們主要分成 網站 跟 App 二個部分來說:
App 登入
- 在 App 中,使用者按下 Sign in with Apple 按鈕,呼叫相關 AuthenticationServices framework 的相關函式,讓 iOS 系統跳出登入確認
- iOS 系統跳出 AppleId 登入確認,利用 TouchId 指紋辨識或 FaceId 臉部辨識(可選擇是否隱藏其 E-mail)
- 登入授權成功,系統呼叫 didCompleteWithAuthorization,我們處理該 Callback 將相關參數,用 API 傳回自己的網站
(如果不做伺服器驗證的話,到這邊就結束了,但不建議) - 在我們的網站 API 收到相關參數後,拿著參數向 Apple 驗證,並取得該使用者資訊
- 拿到使用者資訊之後,進行驗證登入,如果沒有帳號者會自動創立帳號
網站登入
- 在我們的網站,使用者按下 Sign in with Apple 按鈕,呼叫指定網址,其中帶入 Redirect URI 轉址回跳我們網站的網址,瀏覽 Apple 網站進行登入與授權
- 使用者在 Apple 網站進行登入,輸入 AppleId 的帳號密碼,
並授權開發商授權允許(可選擇是否隱藏其 E-mail ) - 登入授權成功,轉跳回我們的網站,並附上一些的參數
- 我們的後端,拿著 Apple 傳回的參數向 Apple 驗證,並取得該使用者資訊
- 拿到使用者資訊之後,進行驗證登入,如果沒有帳號者會自動創立帳號
流程有一點接近但不太一樣,怎麼向 Apple 驗證這個之後會說。
虛擬 E-mail
剛剛有提到,當用戶不願給出自己的 E-mail 時,蘋果可以生成一組
虛擬 E-mail 給用戶使用,
這個虛擬 E-mail 是 亂碼@privaterelay.appleid.com
它是每個 App 專屬的,每一組 E-mail 都不一樣,
蘋果會驗證寄件人與寄件來源(不是什麼信件蘋果都會轉)
刪掉重裝又會產生一組新的虛擬 E-mail,讓使用者可以在他的設定頁,充分控制自己的個資露出於否。
但要實現這個功能,需要設定寄件人,讓指定的寄件人可以寄給使用者。
如果您是用自訂網域的話,則要綁定您的網域,蘋果他會用 SPF (Sender Policy Framework) 來驗證寄件來源。換句話說,如果要讓這個 虛擬 E-mail 能夠正常收到您寄的信,看你網域常用發信服務是誰,在您的網域 DNS 設定對應的 SPF DNS Record。
蘋果開發者網站設定
在 Apple 的開發者網站 ( https://developer.apple.com/ ) 當然要做很多設定
一樣分成網站 跟 App 二個部分來說:
App 部分
在 Certificates, Identifiers & Profiles -> Identifiers -> App IDs
按旁邊 + 號,選擇 App ID,選擇 App (不是 App Clip)
來創建您的 App ID(如果已經建立的話就修改它)
- Description
這裏填入您的 App 名字, - Bundle ID
選擇 Explicit ,並填入 Xcode 裡的 Bundle identifier
(這應該是一般 IOS 開發者會做的事情)
今天要勾選 Sign In with Apple,並按下 Edit
- Sign In with Apple: App ID Configuration
留預設值 Enable as a primary App ID 即可 - Server to Server Notification Endpoint
這裡要填入一個網址,接收處理從 Apple 傳來的通知,後面會提到
網站部分
在 Certificates, Identifiers & Profiles -> Identifiers -> Service IDs
按旁邊 + 號,選擇 Service ID 來創建您的 Service ID
這裡有二個選項要填:
- Description
這裏填入您網站的名字
(注意:這個值會顯示在前台網站給使用者看到) - Identifier
你可以隨意取一個,作為辨識用 - 勾選 Sign in with Apple 並按旁邊的 Configure
在 Web Authentication Configuration 頁面
- Primary App ID
選擇你主要 App 的 App ID -
Register Website URLs
-
Domains and Subdomains
這裏填入您網站的網域
(注意:你的網站必須要有 https,不可用localhost
或者 IP ,否則這裡不會過) -
Return URLs
這裡填入回跳用的 Redirect URI
(一樣的規則,你的網站必須要有 https,不可用localhost
或者 IP ,否則這裡不會過)
如果是測試用的話,可以使用測試用的 Redirect URI:
https://example-app.com/redirect
(他是一個合法的網站,不是亂寫唬爛的)
轉導到 example-app.com 之後藉由您手動複製貼上的方式把回傳值帶到你本機的測試程式中 -
Sign Key 驗證金鑰
我們需要建立一個 Sign Key 在等一下跟蘋果 API 做驗證使用,這部分因為網站跟 App 驗證流程後半段是一樣的,不管支援哪一個部分都要做。
在 Certificates, Identifiers & Profiles -> Keys
按旁邊 + 號,建立一個 Key
- Key name
可以自行取名 - 勾選 Sign in with Apple 並按旁邊的 Configure
- Primary App ID
選擇主要 App 使用的 App ID - Grouped App IDs
取名會把網站跟 App 群組綁在一起
- Primary App ID
按下 Continue 之後會讓你下載一個私鑰 p8 檔案,
注意這只能被下載一次,請好好保存。
如果不見的話就只能再重新產生一個。
設定寄件人、寄件網域
在 Certificates, Identifiers & Profiles -> More -> Sign in with Apple for Email Communication
按旁邊 + 號,輸入您的自訂網域與寄件人。
寄件人可以設定 Gmail 會直接通過。
如果自訂網域,需設定 SPF (DNS Sender Policy Freamwork)
設定 SPF
如果您是用自訂網域的話,則要綁定您的網域,蘋果他會用 SPF (Sender Policy Framework) 來驗證寄件來源。換句話說,如果要讓這個 虛擬 E-mail 能夠正常收到您寄的信,看你網域常用發信服務是誰,在您的網域 DNS 設定對應的 SPF DNS Record。
這遍設定各家有點不一樣
需要新增一個 TXT Record
值為
"v=spf1 include:_spf.google.com include:sendgrid.net include:amazonses.com ~all"
這個是範例值,裡面包含三個郵件服務 Email Service Provider (ESP)
- Google (Gsuite) (Gmail)
- SendGrid
- Amazon SES
看您的郵件服務是哪一家,找到那家業者,照這個方式設定您允許的郵件服務存取您的網域
做一個簡單整理
-
CLIENT ID
它可以是 App ID (也就是 Bundle ID) 也可以是 Service ID。- 如果要 App 端做登入,它就會是 App ID (也就是 Bundle ID)。
- 如果要 網站端做登入,它就會是 Service ID。
-
REDIRECT URI
OAuth 之後要轉跳的網址- App 的部分在 App ID 裡面做設定
Certificates, Identifiers & Profiles -> Identifiers -> App IDs -> 您的 App ID -> Configure -> Return URLs - 網站的部分在 Service ID 裡面做設定
Certificates, Identifiers & Profiles -> Identifiers -> Service IDs -> 您的 Service ID -> Configure -> Return URLs
- App 的部分在 App ID 裡面做設定
-
TEAM ID
你的開發者帳號 Team ID,這可以在你的右上角看到
進去 Certificates, Identifiers & Profiles -> Identifiers -> App IDs -> 您的 App -> App ID Prefix 可以看見 -
KEY ID
您建立驗證的 Sign Key 的 Key ID
在 Certificates, Identifiers & Profiles -> Keys -> 您的 Apple Sign Key -> View Key Details -> Key ID 可以看到 -
SIGN KEY
剛剛產生的 Sign Key 私鑰( p8 檔案 )
App 端實作
這裏你可以使用 Apple 所使用的 Sign In with Apple 按鈕,
Sign In with Apple 的按鈕 這裡
這裡有按鈕樣式規範與素材:
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/
我使用的是直接呼叫的方式
import Foundation
import AuthenticationServices
class AppleSignInManager: NSObject {
var currentView: UIView?
func signIn(currentView: UIView) {
guard #available(iOS 13.0, *) else { return }
self.currentView = currentView
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [.email, .fullName]
request.nonce = "[NONCE]"
request.state = "[STATE]"
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
}
這邊我寫了一個 AppleSignInManager 來控制整個流程,做一個 signIn() 的 method 呼叫 Sign in with Apple。
要設定
- state:一個您設定的,辨識用的字串
- nonce:一個您產生的,辨識用的亂碼
來避免 CRSF 跨網域攻擊
extension AppleSignInManager: ASAuthorizationControllerDelegate {
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController,
didCompleteWithError error: Error) {
// Handle error
print(error)
}
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
// Post identityToken & authorizationCode to your server
print(String(decoding: credential.identityToken ?? Data.init(), as: UTF8.self))
print(String(decoding: credential.authorizationCode ?? Data.init(), as: UTF8.self))
}
}
這邊要實作一個 ASAuthorizationControllerDelegate,處理登入成功、登入失敗之後的動作,
登入成功要把 identityToken
和 authorizationCode
等資訊 回傳給您的伺服器,繼續做驗證。
extension AppleSignInManager: ASAuthorizationControllerPresentationContextProviding {
@available(iOS 13.0, *)
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.currentView?.window ?? UIApplication.shared.keyWindow!
}
}
這邊要實作一個 ASAuthorizationControllerPresentationContextProviding,回傳目前當下 ViewController 的 window 給它,它會在 Sign in with Apple 呼叫後,把系統的畫面放在它上面。
網站前端實作
你可以使用蘋果他預設給你的按鈕,
- 字樣有 Sign in with Apple 或者 Continue with Apple 二種(包含多國語言)可以選
- 樣式有 黑色 跟 白色 二種樣式,還有要邊框 (border) 與否可以選
或者你可以照個他的規範,自訂一個按鈕來用
按鈕樣式的規範與素材在這:
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/
OAuth 跳轉模式有二種可以選:
- 直接頁面轉跳
- 開新視窗(彈窗)登入後關閉(一般 popup window 的做法)
前者較簡單,後者要自行處理流程
文件上提到的寫法就有二種,不過意思是一樣的:
<html>
<head>
</head>
<body>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
<script type="text/javascript">
AppleID.auth.init({
clientId : '[CLIENT_ID]',
scope : '[SCOPES]',
redirectURI : '[REDIRECT_URI]',
state : '[STATE]',
nonce : '[NONCE]',
usePopup : true //or false defaults to false
});
</script>
</body>
</html>
- clientId:這遍填入你的 Services ID
- scope:我都填固定值
name email
(注意:中間有空格) - redirectURI:填入登入成功後要轉跳的網址,必須是 https 不能為 localhost 或者 IP
- state:一個您設定的,辨識用的字串
- nonce:一個您產生的,辨識用的亂碼
- usePopup:是否用開新視窗(彈窗)登入,預設值是否
我使用的是自訂按鈕搭配 JavaScript 直接呼叫的方式:
async function doAppleSignIn() {
try {
const data = await AppleID.auth.signIn();
console.log(data);
} catch (error) {
//handle error.
}
}
上面的 AppleID.auth.signIn()
它會啟動整個 OAuth 流程。
如果是使用彈窗模式的話,會收到回傳的內容,如果是一般模式的話,會用頁面轉跳的。
成功登入時,如果是用轉跳的方式,會轉跳你設定的 REDIRECT_URI
並且傳入幾個參數供使用。
-
code
單次使用的 Authentication code (效期只有 5 分鐘) -
id_token
即identityToken
是一個包含用戶資訊的 JSON web token (JWT) -
state
你定義的字串 -
user
一些 firstName, lastName, email 等使用者資料
(不過蘋果這麼注重用戶隱私,也拿不到什麼資料)
如果是開新視窗登入,需要另外處理事件
//Listen for authorization success
document.addEventListener('AppleIDSignInOnSuccess', (data) => {
//handle successful response
});
//Listen for authorization failures
document.addEventListener('AppleIDSignInOnFailure', (error) => {
//handle error.
});
這裡你可以打你自己設計的 API,
把 identityToken
跟 authorizationCode
傳回給你自己的伺服器繼續做驗證
網站後端實作
JWT (JSON Web Token)
Apple Sign In 的資料交換主要使用 JWT (JSON Web Token) 的格式
(更明確點說,他是一個 JSON Web Signatures (JWS) 的格式)
在這之前,我們先釐清一下一些相關名詞:
- RFC 7519 – JSON Web Token(JWT)
定義了 header 內容與 claim 內容,以及 token 的相關規範- RFC 7515 – JSON Web Signature(JWS)
定義如何做帶有簽章的 token - RFC 7516 – JSON Web Encryption(JWE)
定義內容加密的 token
- RFC 7515 – JSON Web Signature(JWS)
- RFC 7517 – JSON Web Key(JWK)
定義金鑰的格式 - RFC 7518 – JSON Web Algorithms(JWA)
定義加解密的演算法
所以
- JWS 與 JWE 都是屬於 JWT 的一種。
- 如果沒特別說明,則 JWT 皆是指 JWS。
我們再來說明一下什麼是 JWT
JWT 的全名是
JSON Web Token
,是一種基於 JSON 的開放標準(RFC 7519),它定義了一種簡潔(compact)且自包含(self-contained)的方式,用於在雙方之間安全地將訊息作為 JSON 物件傳輸。而這個訊息是經過數位簽章(Digital Signature),因此可以被驗證及信任。可以使用 密碼(經過 HMAC 演算法) 或用一對 公鑰/私鑰(經過 RSA 或 ECDSA 演算法) 來對 JWT 進行簽章。
它是用 .
(點)來分隔,主要有三個部分 base64UrlDecode 的內容:
- Header
- Payload
- Signature/Encryption data
前二者用 Base64UrlDecode 解碼後,各會是一個 JSON 資料,
而 signature 故名思義就是一個用演算法算出來的 Hash
展開說明如下:
-
Header
-
alg
必要欄位,對此 JWT 進行簽章、加解密的主要演算法 (JWA)。
(這個名字叫做 JWA (JSON Web Algorithms) )
這裡列出幾個常見的:HS256
(HMAC-SHA256)RS256
(RSA-SHA256)ES256
(ECDSA-SHA256)
第一項只有單向由同一把金鑰做雜湊 (Hash)。
而後二者為非對稱式加解密,由私鑰進行簽名,由公鑰進行驗證。 -
typ
JWT 本身的媒體類型,在 Sign In with Apple 這裡,
我們使用預設值JWT
。 -
kid
這邊是 Apple 定義的 Sign Key 的 Key ID。
-
- Payload
- iss
Issuer 的簡稱,表示發行者。 - aud
Audience 的簡稱,表示接收者。 - iat
Issued at (time) 的簡稱,即該 JWT 發行的時間,用 Unix timestamp 表示(POSIX 定義的自紀元以來的秒數)。 - exp
Expiration (time) 的簡稱,即該 JWT 過期的時間,格式一樣為 Unix timestamp。 - sub
Subject 的簡稱,用字串(case-sensitive) 或 URI 表示這個 JWT 所夾帶的唯一識別訊息。
- iss
怎麼驗證?
驗證方式有二種方式:
identityToken
用 JWT 的格式定義來驗證是否為 Apple 所簽發的authorizationCode
用 OAuth 的機制向 Apple 伺服器交換並要求 Access Token
前後者不衝突,也可以二者都做,看你的需求。
驗證 IdentityToken
其實從 app 端拿到的 identityToken
它本身也是一個 JWT 格式
payload 用 base64UrlDecode 解開後可以得到類似以下的資料
Header 部分
{
"kid": "86D88Kf",
"alg": "RS256"
}
你可以看到他是用 RS256
(RSA-SHA256) 做加密簽章的,
由蘋果伺服器所擁有的私鑰進行簽名,由蘋果提供的 API 取得公鑰進行驗證。
它的 Key ID 為 86D88Kf
(辨識是蘋果哪一把 Key 簽的,這等下會說)
Payload 部分
{
"iss": "https://appleid.apple.com",
"aud": "com.your.app.id",
"exp": 1596621649,
"iat": 1596621049,
"sub": "001451.3dc436155xxxxxxxxxxxxxxxxxxxx59f.0447",
"c_hash": "iUqI9Vyxxxxxxxxxg-CyoA",
"email": "[email protected]",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1596621049,
"nonce_supported": true
}
取重點說明:
- iss:發行者(issuer)
為https://appleid.apple.com
,蘋果的伺服器 - aud:接收者(audience)
為com.your.app.id
(您的 App Id) - sub:Subject 主題的值,是使用者辨識唯一識別碼
(可作為 使用者 ID 當做判斷依據) - email:使用者的 Email,範例值為虛擬 E-mail,
如果用戶選擇不隱藏 E-mail,這裡就會顯示真實用戶的 E-mail - exp:過期時間
Signature 部份
從 Header 可以知道,
蘋果是用 86D88Kf
這把私鑰用 RS256
(RSA-SHA256) 簽的,等下取得該 86D88Kf
的公鑰就可以驗證它。
取得蘋果的公鑰
這裡蘋果有提供 API 來取得公鑰,
蘋果 API 文件在此:
https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature
用 GET 來打以下網址取得:
https://appleid.apple.com/auth/keys
它是一個標準 JSON Web Key Set (JWKS) 格式,
會得到類似這樣的資料
{
"keys": [
{
"kty": "RSA",
"kid": "86D88Kf",
"use": "sig",
"alg": "RS256",
"n": "iGaLqP..................zHLwQ",
"e": "AQAB"
}
]
}
礙於篇幅縮減了一下資料,重點說明:
- kid:金鑰 ID,你可以找到剛剛範例的
86D88Kf
這個金鑰 - alg:使用的演算法,範例值是
RS256
- use:用途描述,白話文就是拿來幹嘛用的
sig
意思就是拿來簽章用的 - n:RSA 模數 (e),公鑰內容的一部分
- e:RSA 指數 (n),公鑰內容的一部分
簡單來說,我們要:
- 用 kid 找到對應的金鑰,
- 拿 n 跟 e 這二個值還原回 PEM 格式,
- 對 identityToken 做 Signature 驗證,檢核是否為蘋果伺服器發的
- 取用裡面的資料
以下使用 PHP 搭配 Lcobucci/JWT
與 Firebase\JWT
套件來實作
require_once '../vendor/autoload.php';
function getUserDataFromIdentityToken($idToken)
{
$token = (new Lcobucci\JWT\Parser())->parse((string)$idToken);
$applePublicKeysRaw = curlGetAppleAuthKeys();
$applePublicKeys = JWKParseKeySet($applePublicKeysRaw);
$applePublicKey = $applePublicKeys[$token->getHeader('kid')];
$signer = new Lcobucci\JWT\Signer\Rsa\Sha256();
$keychain = new Lcobucci\JWT\Signer\Keychain();
if (!$token->verify($signer, $keychain->getPublicKey($applePublicKey))) {
throw new RuntimeException("Key validation failed.");
}
if ('https://appleid.apple.com' !== $token->getClaim('iss')) {
throw new RuntimeException("Source incorrect.");
}
$userData = array();
$userData['email'] = $token->getClaim('email');
$userData['id'] = $token->getClaim('sub');
return $userData;
}
function curlGetAppleAuthKeys()
{
$ch = curl_init('https://appleid.apple.com/auth/keys');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
function JWKParseKeySet($keySets)
{
$parsed = \Firebase\JWT\JWK::parseKeySet($keySets);
$pemKeySets = array();
foreach ($parsed as $keyId => $sslKey) {
$pemKeySets[$keyId] = openssl_pkey_get_details($sslKey)['key'];
}
return $pemKeySets;
}
function JWKVerify($idToken, $publicKeyPem)
{
$signer = new Lcobucci\JWT\Signer\Rsa\Sha256();
$keychain = new Lcobucci\JWT\Signer\Keychain();
$token = (new Lcobucci\JWT\Parser())->parse((string)$idToken);
return $token->verify($signer, $keychain->getPublicKey($publicKeyPem));
}
composer.json
{
"require": {
"firebase/php-jwt": "5.2.0",
"lcobucci/jwt": "3.3.2"
}
}
使用 AuthorizationCode 交換 AccessToken
這部分也就是最困難的部分
先貼蘋果 API 文件:
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
需要 POST 以下網址:
https://appleid.apple.com/auth/token
它定義了幾個參數
- client_id
如果是 App 登入就是 App ID (Bundle ID),
如果是網站登入就是 Services ID。 - client_secret
一個 JWT 格式的要求文件,並利用前面申請的 Sign Key 來簽章 - code
填入 App 或 網站 收到的 Authorization Code
(注意:Authorization Code 的效期非常短,文件上說最長只有 5 分鐘效期,
但實際設定可能只有 30 秒至 1 分鐘左右) - grant_type
這裡我們填authorization_code
來交換 AccessToken
所以我們要:
- 自己組一個 JWT,並利用前面申請的 Sign Key 來簽章(指定使用
ES256
演算法),作為client_secret
,並帶入收到的 Authorization Code 作為參數, - 打蘋果的 API
蘋果有給一個 JWT 的組成範例:
{
"alg": "ES256",
"kid": "ABC123DEFG"
}
- alg:指定使用
ES256
(ECDSA-SHA256) - kid:你的 Sign Key 的 Key ID
{
"iss": "DEF123GHIJ",
"iat": 1437179036,
"exp": 1493298100,
"aud": "https://appleid.apple.com",
"sub": "com.mytest.app"
}
- iss:發行者(issuer)
填入你的 開發者帳號的 TEAM ID - aud:接收者(audience)
填入固定值https://appleid.apple.com
- iat:填入現在時間 (Unix timestamp)
- exp:填入過期時間 (Unix timestamp),不能超過六個月
- sub:Subject 主題的部分填入上述的 client_id
如果是 App 登入就是 App ID (Bundle ID),
如果是網站登入就是 Services ID。
最後簽章,組成一個 JWT 當作 client_secret 來打 API
成功的話會有類似以下的回傳值
{
"access_token": "a6cab...........Y1A",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "r9d77fa9..........gfYA",
"id_token": "eyJraW.................jMA"
}
其中一個重點, id_token
就是 identityToken
,是我們要的資料,
解開後有:
- sub :Subject 主題的值為 使用者 ID
- email:值為使用者的 Email
這些前面有提到了,就不在重述。
Troubleshooting
雖然文件定義了很多錯誤訊息
https://developer.apple.com/documentation/sign_in_with_apple/errorresponse
大致會出現的只有幾種:
- invalid_request 通常是連參數都弄錯
- invalid_client 可能是
client_id
弄錯,
client_secret
(JWT 格式) 裡的 sub 跟client_id
參數不吻合,或者根本沒這個client_id
- invalid_grant 這個是最常見(也是最難排查的)的錯誤,
- 可能是
client_secret
JWT 格式的加解密算錯 ⚠️ - 可能是 Authorization Code 過期(沒即時打 API 做交換)
(Authorization Code 效期超短,通常只有 1-2 分鐘,不超過 5 分鐘) ⚠️ - 可能是 Authorization Code 已經被交換掉,變成無效 Code ⚠️
(API 帶相同參數重複打就會出現)
- 可能是
Server to Server Notification
這個功能在新的 WWDC 2020 影片中推出,
截稿至今,蘋果還沒有寫 API 文件,只有釋出影片解釋 😓。
詳述在這裡
- Get the most out of Sign in with Apple – WWDC2020
https://developer.apple.com/videos/play/wwdc2020/10173
設定上去之後,蘋果會在用戶有帳號變更的時候通知你。
蘋果會用 POST 傳一個 json 格式給你,格式大致如下:
{
"payload": "eyJraWQxxxxxxxxxxxxxxiUlMyNTYifQ.eyJpcxxxxxxxxxxxxxc0fSJ9.IUFWxPxxxxxxxxxxxxxxxxxbL3olA"
}
只有一個值叫做 payload,裡面也是一個 JWT 格式
你把 JWT 其中的 payload 用 base64UrlDecode 解開,會得到以下格式
{
"iss": "https://appleid.apple.com/",
"aud": "<Bundle Identifier>",
"iat": 1508184845,
"jti": "<unique events stream id>",
"events": [
{
"type": "email-disabled",
"sub": "<user_id>",
"email": "<[email protected]>",
"is_private_email": true,
"event_time": 1508184845
}
]
}
重點說明如下:
- iss:發行者(issuer)
為https://appleid.apple.com
,蘋果的伺服器 - aud:接收者(audience)
為com.your.app.id
(您的 App Id) - iat
Issued at (time) 的簡稱,即該 JWT 發行的時間,用 Unix timestamp 表示。 - jti:事件唯一碼
- events:事件
- type:目前狀態
- sub:用戶唯一碼
- email:用戶電子信箱(有可能是虛擬 Email)
- is_private_email:是否為虛擬 Email
- event_time:事件時間
其中 type
狀態有幾種:
- email-enabled
- email-disabled
- consent-revoked
- account-delete
祝串接順利。 🙂
參考資料
- Introducing Sign In with Apple – WWDC2019
https://developer.apple.com/videos/play/wwdc2019/706/ - Get the most out of Sign in with Apple – WWDC2020
https://developer.apple.com/videos/play/wwdc2020/10173 - [PHP] OAuth / Sign in with Apple JS – 使用 Apple JS SDK 讓網站支援 Apple ID 登入
http://blog.changyy.org/2019/11/php-jwt-oauth-sign-in-with-apple-js.html - What the Heck is Sign In with Apple?
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple - 如何整合 Sign in with Apple 到自己的 iOS App 上 (iOS & Backend)
https://medium.com/@tuzaiz/%E5%A6%82%E4%BD%95%E6%95%B4%E5%90%88-sign-in-with-apple-%E5%88%B0%E8%87%AA%E5%B7%B1%E7%9A%84-ios-app-%E4%B8%8A-ios-backend-e64d9de15410 - Sign in with Apple(蘋果授權登陸)
https://blog.csdn.net/wpf199402076118/article/details/99677412 - Sign in with Apple 登錄詳解
https://ihtcboy.com/2019/09/16/2019-09-16_Sign-in-with-Apple/ - iOS13 Sign In With Apple 適配
http://jerryliu.org/ios%20programming/iOS13-Sign-With-Apple%E6%96%B0%E7%89%B9%E6%80%A7%E9%80%82%E9%85%8D - 蘋果Sign In with Apple爆可被劫持帳號的漏洞
https://www.ithome.com.tw/news/137972 - 蘋果重磅推出「Sign in with Apple」登入,背後忽略的隱私盲點
https://www.bnext.com.tw/article/53765/what-is-the-meaning-of-sign-in-with-apple - [筆記] 透過 JWT 實作驗證機制
https://medium.com/%E9%BA%A5%E5%85%8B%E7%9A%84%E5%8D%8A%E8%B7%AF%E5%87%BA%E5%AE%B6%E7%AD%86%E8%A8%98/%E7%AD%86%E8%A8%98-%E9%80%8F%E9%81%8E-jwt-%E5%AF%A6%E4%BD%9C%E9%A9%97%E8%AD%89%E6%A9%9F%E5%88%B6-2e64d72594f8 - 是誰在敲打我窗?什麼是 JWT ?
https://5xruby.tw/posts/what-is-jwt/ - JWT 簽名算法 HS256、RS256 及 ES256 及密鑰生成
https://www.cnblogs.com/kirito-c/p/12402066.html - 驗證 JSON Web Token
https://docs.aws.amazon.com/zh_tw/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html - Sign in with Apple
http://uirate.net/?p=10363 - Ensure mail delivery & prevent spoofing (SPF)
https://support.google.com/a/answer/33786 - DNS 設定 spf 記錄 – Sender Policy Framework
https://blog.xuite.net/tolarku/blog/233356505-DNS+%E8%A8%AD%E5%AE%9A+spf+%E8%A8%98%E9%8C%84+-+Sender+Policy+Framework - Generate and Validate Tokens
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens - 簡介 JWK 與 JWA
https://ithelp.ithome.com.tw/articles/10225590 - Sign in with Apple Tutorial, Part 4: Web and Other Platforms
https://sarunw.com/posts/sign-in-with-apple-4/ - JWT 概觀
https://ithelp.ithome.com.tw/articles/10224787 - [Note] OAuth2.0 學習筆記 | PJCHENder 未整理筆記
https://pjchender.github.io/2017/11/16/note-oauth2-0-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98/ - 簡單易懂的 OAuth 2.0
https://speakerdeck.com/chitsaou/jian-dan-yi-dong-de-oauth-2-dot-0 - 關於OAuth 2.0-以Facebook為例
https://medium.com/@justinlee_78563/%E9%97%9C%E6%96%BCoauth-2-0-%E4%BB%A5facebook%E7%82%BA%E4%BE%8B-6f78a4a55f52