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

AspectStandard / GoogleApple
Client secretStatic stringJWT signed with ES256, regenerated periodically
Scopesopenid, email, profile, phone, addressOnly name and email (plus implicit openid)
User info deliveryIn ID token + /userinfo endpointIn authorization response body (form post), NOT in ID token
User info availabilityAlways availableFirst authorization only
EmailUser’s real emailOptional private relay address
Response modeStandard redirect (response_mode=query)Requires form_post when name/email scopes are requested
PKCE supportYesHistorically not supported — verify current state
/userinfo endpointStandardNone

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 user field.

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:

  1. Persist the user JSON to a durable store before doing any other work in the callback.
  2. 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.com per 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-Agent header. 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: code and id_token together in the authorization response when name/email scopes are requested.

Validation

Same as any ID Token:

  • iss == "https://appleid.apple.com"
  • aud == your Service ID
  • exp not past
  • Signature: fetch https://appleid.apple.com/auth/keys (JWKS), match by kid, verify with RS256.
  • nonce_supported claim — Apple includes this; verify your nonce matches 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 email as a stable identifier when it could be a relay address that the user revokes.

See Also