Sign in with Apple: The Engineering Decisions That Matter Before You Ship
Sign in with Apple is easy to add and easy to get wrong. Here's how to handle one-time profile data, server-side JWT validation, and the private email relay before they bite you in production.
13 Jul 2026, 06:30 UTC

If your iOS app offers Google or Facebook login, Apple has historically expected you to offer Sign in with Apple too — that's an App Store Review Guidelines consideration, not a suggestion. But the policy pressure is actually the least interesting part. The real work is deciding how to handle three things Apple does differently from every other identity provider: it gives you profile data exactly once, it lets users hide their email behind a relay, and it hands you a JWT you're responsible for validating yourself.
Get those three right and Sign in with Apple is genuinely pleasant: low signup friction, strong platform trust, and no password reset flow to maintain. Get them wrong and you'll discover the problems in production, usually as locked-out users.
The authorization flow, in one pass
On the client, everything goes through the AuthenticationServices framework. You create an ASAuthorizationAppleIDProvider, build an ASAuthorizationAppleIDRequest, and present the authorization sheet. Two details matter here:
- Request name and email scopes only if you need them — and capture them immediately. Apple returns the user's full name and email only on the first authorization. On subsequent attempts, the credential comes back without them. If your backend doesn't persist that data on first login, it's gone; you cannot re-request it later without the user revoking and re-authorizing.
- Generate a cryptographic nonce before presenting the sheet. You send the SHA-256 hash of the nonce with the request, and Apple embeds it in the identity token. Your server later checks that the token's nonce claim matches the hash of the nonce it was expecting. This binds the token to this specific login attempt and blunts replay attacks.
What you get back is an ASAuthorizationAppleIDCredential containing an identityToken (a JWT), a short-lived authorizationCode, and — on first run only — the name and email.
Never trust the client callback
The delegate method firing successfully on the device proves nothing to your backend. The client sends the identityToken and the raw nonce to your server over TLS, and the server does the real work:
- Decode the JWT header to get the key ID (
kid), then fetch Apple's public keys from their published JWKS endpoint and find the matching key. Cache the keys, but refetch on an unknownkid— Apple rotates them. - Verify the signature against that key.
- Check the claims: issuer is Apple, audience (
aud) matches your app's bundle ID or services ID, expiry (exp) is in the future, and thenonceclaim equals the SHA-256 hash of the nonce you generated for this attempt. - Only then issue your own session token or cookie.
The stable identifier is the JWT's sub claim — the Apple user ID. Build your account model around that, not the email address.
Design around the private email relay
Users can choose "Hide My Email," and Apple routes messages through a private relay address like [contact removed]. Two consequences:
- Account linking by email will break. A user who signed up with a real email via another provider and later uses Sign in with Apple with a hidden email looks like two different people. Link on the Apple
sub, and if you need to merge accounts, do it explicitly with an authenticated flow. - Deliverability needs setup. To email relay addresses, your sending domain or address must be registered in your Apple Developer account, and messages must pass SPF/DKIM. Skip this and your transactional mail silently disappears.
Also note the user ID is scoped to your developer team in typical configurations. If you ever transfer the app to another team, users receive a different sub — Apple provides a transfer mechanism, but it requires planning, not a hope-for-the-best migration.
Worked example: the login round trip
Concretely, a first-login flow looks like this:
// iOS client (runs in app, no special entitlements beyond the
// Sign in with Apple capability enabled in the target)
let rawNonce = Self.randomNonceString()
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(rawNonce) // hash goes to Apple
// On success, POST to your backend:
// { "identityToken": "<jwt>", "nonce": "<rawNonce>", "fullName": ..., "email": ... }# Backend (runs on your server, needs outbound HTTPS to fetch Apple's JWKS)
# 1. GET Apple's public keys, select by jwt.header.kid
# 2. verify_signature(identityToken, applePublicKey)
# 3. assert claims: iss, aud == YOUR_BUNDLE_ID, exp > now,
# nonce == sha256(body.nonce)
# 4. upsert user keyed by claims.sub; persist name/email if present
# 5. issue your own session tokenThe check that catches the most bugs in practice: confirm your backend rejects tokens with the wrong audience, an expired exp, a mismatched nonce, and an unknown kid. Write those as negative tests, not assumptions.
The trade-off you're accepting
You're exchanging profile richness for friction reduction. You get a verified email (maybe), a name (once, maybe), and a stable ID — no birthday, no friends list, no avatar. You also take on revocation handling: users can stop Sign in with Apple from their device Settings, so your app should check credential state on launch and your backend should treat every login as fresh validation rather than assuming a long-lived relationship.
Before you ship
Test with a sandbox Apple ID against four scenarios: first authorization (confirm name/email persisted), repeat authorization (confirm you cope without them), Hide My Email (confirm mail actually arrives), and revocation from Settings (confirm the app degrades gracefully). Then re-read the current AuthenticationServices documentation and App Store Review Guidelines — both the API surface and the policy details can shift, and this is one area where a stale assumption gets your update rejected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.