How do you validate a Sign in with Apple identity token on a server, and what is the nonce for?
Predict first
Which tokens pass the claim checks?
import Foundation
import CryptoKit
func b64url(_ d: Data) -> String { d.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") }
func hex(_ d: some Sequence<UInt8>) -> String { d.map { String(format: "%02x", $0) }.joined() }
// Client: a random nonce stays in memory; its SHA-256 goes in the request.
let rawNonce = "n-0S6_WzA2Mj"
let requestNonce = hex(SHA256.hash(data: Data(rawNonce.utf8)))
print("nonce sent to Apple is", requestNonce.count, "hex characters")
// Server: a made-up identity token, only the claims segment matters here (a real token is signed with Apple's RS256 key).
let claims = #"{"iss":"https://appleid.apple.com","aud":"com.example.bank","sub":"001234.abcdef.0987","nonce":"\#(requestNonce)","email":"asha@privaterelay.appleid.com","email_verified":"true","is_private_email":"true","exp":1700003600}"#
let token = b64url(Data(#"{"alg":"RS256","kid":"demo"}"#.utf8)) + "." + b64url(Data(claims.utf8)) + ".signature-not-checked-here"
func decode(_ segment: Substring) -> [String: Any] {
var t = segment.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
while t.count % 4 != 0 { t += "=" }
return try! JSONSerialization.jsonObject(with: Data(base64Encoded: t)!) as! [String: Any]
}
func check(_ token: String, rawNonce: String, now: Int) -> String {
let c = decode(token.split(separator: ".")[1])
guard c["iss"] as? String == "https://appleid.apple.com" else { return "reject: issuer" }
guard c["aud"] as? String == "com.example.bank" else { return "reject: audience" }
guard (c["exp"] as! Int) > now else { return "reject: expired" }
guard c["nonce"] as? String == hex(SHA256.hash(data: Data(rawNonce.utf8))) else { return "reject: nonce (replay?)" }
return "accept sub=\(c["sub"]!)"
}
print(check(token, rawNonce: rawNonce, now: 1_700_000_000))
print(check(token, rawNonce: "another", now: 1_700_000_000))
print(check(token, rawNonce: rawNonce, now: 1_700_009_999))
print("segments:", token.split(separator: ".").count, "| private relay address:", (decode(token.split(separator: ".")[1])["email"] as! String).hasSuffix("@privaterelay.appleid.com"))
- Only the matching nonce within expiry is accepted
- All are accepted
- The wrong nonce is accepted
Mental model. The nonce is a sealed envelope number: only the person who wrote it can prove the reply answers their letter.
The identity token is a JWT. The server verifies the signature with Apple’s public keys (published at the keys endpoint, chosen by kid, RS256) and then the claims: iss is https://appleid.apple.com, aud is your app or service id, exp is in the future, and nonce matches. The client generates a random raw nonce, sends its SHA-256 hex in the request, and the server (which received the raw nonce from the client) hashes it and compares to the claim, which stops a stolen token being replayed in another session. The run checks the claims of a made-up token and rejects a wrong nonce and an expired token; the sub is the stable user identifier. The signature step was not run (it needs Apple’s live keys), so this shows the claim logic only.
User-facing details: the email may be a private relay address (is_private_email), name and email are delivered only on the first authorisation (store them then), email_verified may arrive as a string or a boolean, and credential state should be rechecked at launch (credentialState(forUserID:)) because the user can revoke it. Use the authorization code and refresh flow for long sessions.
Issuer, audience, expiry and nonce must all match.
Common traps
- Skipping the signature check. Decoding a JWT is not verifying it.
- Expecting the email every time. It comes once.
They might follow up with
- What if the user revokes? Handle the credential-revoked notification and sign out.
Related questions
- 17.5.1 How do passkeys, Sign in with Apple and ASWebAuthenticationSession work in AuthenticationServices?
- 17.12.1 What must a server verify when it receives a passkey assertion?
← What must a server verify when it receives a passkey assertion? How big can a Live Activity update be, and what do update and end… →