Sign in with Apple 會送出四種通知,不是三種
Apple 針對 Sign in with Apple 伺服器對伺服器通知發布的開發者公告,列出您的端點會收到的三件事:電子郵件轉寄偏好設定的變更、使用者在您 App 內刪除帳號,以及永久刪除 Apple Account。3 但 API 的官方文件定義了四種不同的事件類型。1
照公告寫成的實作只會處理三個分支,並悄悄丟掉第四種事件。 公告把 email-enabled 與 email-disabled 併成同一條「轉寄偏好設定」的項目。實際上它們是兩則獨立的通知,各自帶著不同的 type 值;而只用 switch 判斷 type、卻沒有 default 分支的程式碼,就會忽略作者漏掉的那一種。
四種事件裡還有兩種的含意超出字面,其中一種甚至會改變 App 的驗證狀態。
重點摘要
Sign in with Apple 會送出四種伺服器對伺服器通知:email-enabled、email-disabled、consent-revoked 與 account-deleted。1 內容以 JWS 形式包在一個 JSON 物件的 payload 鍵底下,由 Apple 的私密金鑰簽章;在採取任何行動之前,必須依標頭 alg 參數所指定的演算法完成驗證。1 consent-revoked 會讓使用者的憑證失效,因此它是驗證事件,而非偏好設定變更。原生 App 在 Apple Account 遭刪除時收不到任何用戶端回呼,伺服器通知是唯一的訊號。1 至於重送與投遞語意,Apple 完全沒有寫進文件。
四種事件類型
每則通知都在 events 宣告(claim)裡帶一個 type 值。1
type |
發生了什麼 |
|---|---|
email-enabled |
使用者透過「隱藏我的電子郵件」啟用了轉寄到個人信箱的功能 |
email-disabled |
使用者停用了電子郵件轉寄 |
consent-revoked |
使用者撤銷了對您 App 的授權,其憑證隨即失效 |
account-deleted |
使用者要求永久刪除自己的 Apple Account |
被公告合併掉的,正是這兩個電子郵件事件。它們各有各的意義:email-disabled 代表您寄往轉寄位址的信件不再送達使用者,email-enabled 代表恢復送達。把兩者當成同一個「偏好設定已變更」事件,等於逼自己再回頭查詢目前狀態,而通知本身早就告訴您了。
兩種不能只看字面的事件
consent-revoked 是驗證事件。 Apple 的說明是,使用者「撤銷了您的 App 使用其 Apple Account 的授權,其憑證隨即失效」。1 不是即將淘汰,也不是等待過期,而是失效。
若把這則通知和電子郵件事件放在一起記錄、順手更新一列偏好設定,App 就會繼續呈現已登入的介面,背後卻是一組再也無法通過驗證的憑證。使用者會一直看到自己的帳號,直到下一次 token 更新失敗——然後看到更糟的畫面。正確的處理方式是結束該會話並導向重新驗證,就走您處理被撤銷 OAuth 授權時的同一條路徑。
account-deleted 可能是您唯一會收到的通知。 Apple 說明,當使用者永久刪除 Apple Account 時,Sign in with Apple 會讓所有使用者 token 失效,並停用所有相關 App 的電子郵件轉寄;而對原生 App,系統不會送出用戶端回呼。1
這句話正是「到底該不該架設端點」最有力的理由。只做 iOS、沒有伺服器對伺服器端點的團隊,根本沒有任何管道得知帳號已經消失。紀錄留在原地,轉寄位址失效,而您該履行的刪除義務也無從履行——因為沒有任何人告訴您有東西該刪。
註冊端點
設定的位置在 Certificates, Identifiers & Profiles:選擇 Identifiers,挑出您的 App ID,啟用 Sign in with Apple 服務,點選 Configure,然後填入端點 URL。2
在依這些限制設計架構之前,值得先把它們讀清楚。2
- 每個 Sign in with Apple App 群組與金鑰只能有一個 URL。 不是每個 App 一個。
- 只能註冊在主要 App ID 上。
- URL 必須是包含 scheme、host 與 path 的絕對 URI:
https://example.com/path/to/endpoint - 接收通知必須使用 TLS 1.2 以上版本。
API 的官方文件另外提到,同一個 URL 可以給多個開發團隊與多個 App 使用。1 對照「每個群組一個 URL」的規則,合理的解讀是:單一服務可以接收全部通知,各個群組則各自註冊指向它的位址。Apple 並未說明兩者如何互動,所以請把共用端點視為可行、而非官方背書;在真正依賴它之前,先確認您的處理常式能分辨每則通知屬於哪個 App。
TLS 1.2 這道下限,背後連著一個更大的轉向。OS 27 開始對管理流量強制執行更嚴格的 TLS 要求,同樣以 1.2 為最低標準,另外還要求符合 ATS 的加密套件與憑證。今天滿足 Apple 要求的端點,不代表自動符合 ATS,而整體走向是愈收愈緊,不是放寬。
解讀 payload 內容
投遞方式是一則 HTTP POST,請求主體是一個 JSON 物件,簽章後的 token 就放在裡面:1
{
"payload": "<SERVER_TO_SERVER_NOTIFICATION_JWS>"
}
JWS 是被包起來的,不是裸的。 先解析 JSON、取出 payload,再進行驗證。把整個請求主體直接丟給 JWS 驗證器的實作,會在第一則通知就失敗,而且錯誤會表現成 token 格式有問題,而不是包裝方式弄錯——於是您會往錯的方向找。
驗證先於解讀。內容由 Apple 的私密金鑰以 JSON Web Signature 格式簽章,Apple 的指示是檢視該 JWS,並使用標頭 alg 參數所指定的演算法驗證簽章。1 簽章確認無誤之後,才讀取 events 宣告並依 type 分支。
有兩個一般 JWS 實務的習慣值得保留:絕不信任會讓呼叫端降級驗證的 alg 值;並確認 token 的簽發者與受眾符合預期,而不是照單全收任何格式正確、由 Apple 簽章的 token。
解碼後的結構
通過驗證後,一則解碼的 consent-revoked 通知長這樣:1
{
"iss": "https://appleid.apple.com",
"aud": "com.mytest.app",
"iat": 1508184845,
"jti": "abede...67890",
"events": {
"type": "consent-revoked",
"sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219",
"event_time": 1508184845
}
}
account-deleted 帶的欄位相同,只有 type 不同。電子郵件事件則多出兩個欄位:email 與 is_private_email。
這個結構裡有三個細節,臨場才遇到會白白吃掉您的時間。
events 是物件,不是陣列。 名稱是複數,值卻只有一個事件。照名稱而不是照結構寫的程式碼,會去迭代一個字典,然後拿到一堆鍵。
is_private_email 是字串。 Apple 的範例寫的是加了引號的 "true",而不是 JSON 的布林值 true。嚴格的解碼器把它對應到 Bool 會直接失敗;寬鬆的解碼器把任何非空字串都當成真,則是碰巧答對,接著就會在 "false" 上答錯。
sub 是穩定的使用者識別碼,和登入時收到的是同一個值,也是您用來找出這則通知對應哪個帳號的依據。aud 則是您的用戶端識別碼,共用端點就是靠它把通知分派給不同的 App。
關於 Apple 自己的範例,還有一點要提醒:兩則電子郵件事件的內容在 "is_private_email": "true" 與 "event_time" 之間漏了一個逗號。把任一段複製進 JSON 解析器,都會被判定為無效文件。結構是對的,標點不對;把它貼進測試資料的讀者,會為了一個不是自己造成的語法錯誤損失十分鐘。
Apple 的用語也有出入。內文說內容採 JSON Web Signature 格式,外層包裝的範例卻把值命名為 SERVER_TO_SERVER_NOTIFICATION_JWT。1 兩者指的是同一個東西;簽章後的 JWT 就是內容為 JSON 的 JWS。在文件裡搜尋而兩種說法都撞見時,知道這一點會省事許多。
一個完整的處理常式
從上述限制,就能推出正確處理常式的樣貌。以下用 Python 搭配 PyJWT:
import json
import jwt
from jwt import PyJWKClient
# Apple publishes its signing keys as a JWKS. Cache the client;
# it fetches and caches keys rather than hitting Apple per request.
JWKS = PyJWKClient("https://appleid.apple.com/auth/keys")
CLIENT_ID = "com.mytest.app" # your aud value
def handle_notification(request_body: bytes):
# 1. The JWS is wrapped in JSON under "payload", not the raw body.
wrapper = json.loads(request_body)
token = wrapper["payload"]
# 2. Resolve the signing key by the token's kid, then verify.
# Pin the algorithm. Never read alg from the token to decide.
signing_key = JWKS.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer="https://appleid.apple.com",
)
# 3. Only now is anything trustworthy.
event = claims["events"] # an object, not a list
apple_user_id = event["sub"] # stable identifier from sign-in
match event["type"]:
case "consent-revoked" | "account-deleted":
end_all_sessions(apple_user_id)
mark_account_unlinked(apple_user_id)
case "email-disabled":
set_email_forwarding(apple_user_id, enabled=False)
case "email-enabled":
set_email_forwarding(apple_user_id, enabled=True)
case other:
log_unknown_event(other) # do not fail silently
其中有幾行是關鍵。
固定演算法。 Apple 目前上線的 JWKS 公開三把 RSA 金鑰,全部是 RS256、use: sig,並帶有不同的 kid。傳入 algorithms=["RS256"],而不是從 token 讀取 alg,就堵住了經典的降級路徑:攻擊者送來一個宣稱 alg: none 的 token。
依 kid 尋找金鑰,別直接拿第一把。 三把金鑰同時有效,這正是金鑰輪替從外部看起來的樣子。抓 keys[0] 的處理常式在 Apple 輪替之前都能運作,之後就會對部分 token 失敗,而那是非常難查的問題。
驗證 aud 與 iss。 少了這兩項,您會接受任何簽章正確的 Apple token,包括為別的 App 簽發的那些。
保留 default 分支。 否則第五種事件類型就會憑空消失。照 Apple 那則三項公告寫成的實作,今天之所以漏掉 email-enabled 或 email-disabled,正是這麼來的。
Apple 沒有寫進文件的部分
無論是 API 的技術文件,還是帳號說明頁面,都沒有提到任何投遞保證。在兩份文件中搜尋「重送」「重新投遞」「確認回覆」「狀態碼」「逾時」與「冪等性」等字眼,一無所獲。
因此,截至撰稿時,Apple 的文件並未回答以下問題:
- 投遞失敗會不會重送,重送幾次
- 若會重送,時間窗有多長
- 端點該回傳哪個狀態碼才算成功
- 同一則通知會不會送達兩次
這個空白帶來設計上的後果,而以下是我在 Apple 明文之外的推論,不是轉述。投遞語意未定義的端點,不能當成權威的事件流。防禦性的做法是:把每則通知視為「有東西變了」的提示,再回頭與自己的紀錄核對,而不是盲目套用事件。處理常式要做到冪等,因為您無法排除重複投遞。也不要建立一套「必須收齊每一則通知才會正確」的流程,因為您無從確認自己真的收齊了。
如果端點停擺一小時,您無法從文件得知,究竟是遺失了一小時的帳號刪除事件,還是它們正排在某處等著送。就當作已經遺失來設計。
也沒有任何官方的測試方式
在這兩份頁面搜尋「沙箱」「模擬」與「觸發」等字眼,同樣沒有結果。Apple 沒有提供任何按需觸發通知的機制。
於是形成一個尷尬的循環。最要緊的兩種事件——consent-revoked 與 account-deleted——都得由使用者撤銷對您 App 的授權,或刪除自己的 Apple Account 才會產生。要用真實的 account-deleted 驗證處理常式,就代表得有人真的刪掉一個 Apple Account。這種測試沒有人做第二次。
可行的替代方案是把問題拆成兩半。自己依前面的結構組出解碼後的內容,用單元測試檢驗分支邏輯、冪等性與核對邏輯。傳輸與簽章這條路徑則另外驗證,用一則您真的產得出來的通知:撤銷測試用 Apple Account 的授權是可以復原的,刪除帳號不是,而且這麼做會從頭到尾走過外層解析、kid 查詢與簽章檢查。
無論如何,在註冊之前,先確認端點可連線,而且能迅速回應。註冊了卻從未驗證的端點,正是某個團隊在數個月後才發現的事——上線以來的每一則通知,都送到了一個憑證早已過期的 URL。
一項已經生效的規定
這件事之所以會出現在開發者新聞裡,原因是:自 2026年1月1日起,位於大韓民國的開發者在註冊新的 Services ID 或更新既有的 Services ID 時,若要以 Sign in with Apple 將網站與 App 建立關聯,就必須提供伺服器對伺服器通知端點。3 Apple 於 2025年10月9日公布這項規定。
規定的適用範圍很窄,而且已經生效數月,不是還要準備的未來式。它真正的價值在於方向。Apple 已經開始在至少一個司法管轄區把端點列為必要,而背後的理由具有普遍性:讓人們掌控自己分享出去的個人資料,並讓帳號刪除真正傳遞下去。這套邏輯沒有一處只適用於韓國。
如果您反正都要做這個端點,就趁主管機關還沒把它變成您的期限之前先做完。
重點整理
給後端工程師:
- 要處理四種類型,不是三種。email-enabled 與 email-disabled 是分開送達的。
- 先解析 JSON 主體、取出 payload,再把東西交給 JWS 驗證器。
- 讀取 events 宣告之前,先用標頭 alg 指定的演算法驗證簽章。
- 讓處理常式具備冪等性,並與自己的紀錄核對。Apple 沒有提供任何投遞保證。
給 iOS 團隊:
- consent-revoked 會讓憑證失效。請把它當成結束會話的驗證事件,而不是偏好設定更新。
- 原生 App 在 Apple Account 遭刪除時收不到用戶端回呼。沒有端點,您永遠不會知道這件事發生過。
給還在考慮值不值得做的人: - 自 2026年1月起,這個端點對韓國開發者已是強制規定,而背後的理由具有普遍性。
常見問題
通知類型總共有幾種?
四種:email-enabled、email-disabled、consent-revoked 與 account-deleted。1 Apple 開發者新聞的公告寫的是三種,把兩個電子郵件事件併成同一條關於轉寄偏好設定的項目。3
consent-revoked 對我的會話代表什麼?
使用者的憑證會失效。1 請比照被撤銷的 OAuth 授權來處理:結束會話並讓使用者重新驗證,而不是更新一個偏好設定就繼續下去。
如果我只推出原生 iOS App,還需要端點嗎?
Apple 說明,Apple Account 被永久刪除時,系統不會對原生 App 送出用戶端回呼。1 沒有伺服器對伺服器端點,就沒有任何機制會通知您。
端點該回傳什麼?如果它掛掉了怎麼辦?
Apple 沒有說明狀態碼的期待、重送行為,也沒有說是否會重複投遞。請以「至少一次、也可能至多一次」的投遞前提來設計,讓處理常式具備冪等性,並與自己的紀錄核對,而不是假設每則通知都送到了。
一個端點可以服務多個 App 嗎?
Apple 的文件寫道,同一個 URL 可以用於多個開發團隊與多個 App。1 而註冊的規則是:每個 Sign in with Apple App 群組與金鑰一個 URL,且必須在主要 App ID 上。2 只要您的處理常式能判斷每則通知屬於哪個 App,共用服務就是可行的。
資料來源
-
Apple,“Processing changes for Sign in with Apple accounts.” 四種事件類型(
email-enabled、email-disabled、consent-revoked、account-deleted)、JWS 內容格式與「依標頭alg參數驗證」的指示、{"payload": "<JWS>"}的包裝方式、撤銷授權會使憑證失效的說明、原生 App 在帳號刪除時收不到用戶端回呼的注記、TLS 1.2 的伺服器要求,以及同一個 URL 可跨多個團隊與 App 使用的允許事項,皆出自此處。擷取日期:2026年8月2日。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple,“Enabling server-to-server notifications.” 透過 Certificates, Identifiers & Profiles 註冊的路徑、每個 App 群組與金鑰一個 URL 的規則、主要 App ID 的限制、絕對 URI 要求與 TLS 1.2 要求,皆出自此處。擷取日期:2026年8月2日。 ↩↩↩
-
Apple Developer News,“New requirement for apps using Sign in with Apple for account creation,” 2025年10月9日。韓國自 2026年1月1日生效的規定,以及端點接收內容的三項摘要,皆出自此處。 ↩↩↩