Sign in with Apple
Apple’s OpenID Connect implementation. Spec-adjacent but with several practical deviations that bite implementers. Read this before writing the integration.
Verify before relying
Apple changes details quietly (PKCE support, scope behavior, response modes). Re-check Apple’s docs against the items below before shipping. The notes below were accurate as of the original research; some may have shifted.
Deviations from Standard OIDC
| Aspect | Standard / Google | Apple |
|---|---|---|
| Client secret | Static string | JWT signed with ES256, regenerated periodically |
| Scopes | openid, email, profile, phone, address | Only name and email (plus implicit openid) |
| User info delivery | In ID token + /userinfo endpoint | In authorization response body (form post), NOT in ID token |
| User info availability | Always available | First authorization only |
| User’s real email | Optional private relay address | |
| Response mode | Standard redirect (response_mode=query) | Requires form_post when name/email scopes are requested |
| PKCE support | Yes | Historically not supported — verify current state |
/userinfo endpoint | Standard | None |
The Client Secret JWT
Apple does not issue a static client secret. You generate a JWT, signed with a private key Apple gives you in the Developer Portal:
Header: { "alg": "ES256", "kid": "<your key id>" }
Payload:
iss: <your Apple Team ID>
iat: <now>
exp: <max 6 months from iat>
aud: "https://appleid.apple.com"
sub: <your Service ID — acts as client_id>Sign with the ES256 private key. The resulting JWT is what you put in client_secret when calling the token endpoint. Cache and rotate it (typically generate one valid for weeks/months and refresh ahead of expiry).
The First-Authorization-Only Catch
If your callback fails on the first authorization, you lose the user's name forever.
Apple sends the user’s name and email in the form-post body of the authorization response, but only the first time the user authorizes your app. On every subsequent authorization, the response contains the code and ID token but no
userfield.If your server crashes, your callback throws, or you redirect away before persisting that data, the only recovery path is to have the user revoke your app in their Apple ID settings and re-authorize.
Defense:
- Persist the
userJSON to a durable store before doing any other work in the callback. - Idempotency: re-running the callback for the same code returns nothing useful, so persistence must happen on first parse, not after token exchange.
Private Email Relay
Users can choose:
- Share My Email → Apple sends your app the user’s actual email.
- Hide My Email → Apple generates a unique address like
abc123@privaterelay.appleid.comper app. Mail you send there is forwarded to the user’s real email.
To send mail to relay addresses, your sending domain (and IPs) must be registered with Apple. Otherwise the relay drops your messages.
The user can disable forwarding per-app at any time. Your app sees the same relay address until they do.
Response Mode: form_post
When you ask for name email scopes, Apple requires response_mode=form_post. Apple POSTs the response back to your redirect_uri as application/x-www-form-urlencoded, not as a query string. Your callback must accept POST.
POST /callback
Content-Type: application/x-www-form-urlencoded
code=...&id_token=...&state=...&user={"name":{"firstName":"...","lastName":"..."},"email":"..."}(user is JSON-encoded inside the form parameter — parse twice.)
Other Quirks
- Apple’s token endpoint requires a
User-Agentheader. Default HTTP libraries usually send one; if yours doesn’t, you’ll get opaque errors. - No
localhost— for development you need a real HTTPS hostname (use a tunnel like ngrok with a registered redirect URI). - The discovery document is incomplete: it doesn’t list every supported feature. Apple’s web docs are the source of truth.
- Returns a hybrid response:
codeandid_tokentogether in the authorization response when name/email scopes are requested.
Validation
Same as any ID Token:
iss == "https://appleid.apple.com"aud == your Service IDexpnot past- Signature: fetch
https://appleid.apple.com/auth/keys(JWKS), match bykid, verify with RS256. nonce_supportedclaim — Apple includes this; verify yournoncematches if you sent one.
Pitfalls (Recap)
- Losing the user’s name because the first callback failed.
- Forgetting to register your mail domain with Apple → mail to relay addresses silently drops.
- Generating a client secret JWT that lasts past 6 months — Apple rejects it.
- Building a GET-only callback handler.
- Treating
emailas a stable identifier when it could be a relay address that the user revokes.