7.12 — Sign in with Apple
Opening scenario
Your app’s signup screen currently has Google, Facebook, email/password — and you just got an App Review rejection: App Store Review Guideline 4.8 (“apps that use a third-party login service must also offer Sign in with Apple as an equivalent option”). The fix is one button and one delegate, plus a server change to accept Apple’s identity token. SIWA also delivers something Google/Facebook can’t: email obscuring (the user gets a xxx@privaterelay.appleid.com proxy address that forwards to their real inbox), no tracking, no email scraping — making it the highest-conversion auth choice for privacy-conscious users.
| Context | What it usually means |
|---|---|
| Reads “ASAuthorizationController” | Has shipped the button |
| Reads “identity token / authorization code” | Knows the token model |
| Reads “credential state” | Has handled sign-out / Apple ID changes |
| Reads “server-side verification” | Has a backend that validates Apple JWTs |
| Reads “App Transfer” | Has migrated apps with active SIWA users |
Concept → Why → How → Code
Concept
Sign in with Apple is an OAuth-flavored identity protocol with three concrete deliverables for your app:
user— a stable opaque identifier per (Apple ID, app team).identityToken— a signed JWT containing the user identifier and (on first sign-in) the email. Your server verifies this against Apple’s public keys.authorizationCode— a one-time code your server can exchange with Apple’s token endpoint for a refresh token (for long-lived server-side sessions).
You only get the user’s full name and email on the first sign-in. After that, Apple stops sending those fields. Your server must persist them on first contact.
Why
- Guideline 4.8 compliance — if you use any third-party auth, SIWA is required.
- Privacy story — the email-relay feature (“Hide My Email”) is genuinely useful and trusted.
- Conversion — Apple-ID users tap one button and they’re in. No email confirmation flow, no password.
- Cross-platform — works on iOS, macOS, watchOS, tvOS natively; on web/Android via the Sign in with Apple JS SDK with the same identity.
How — the button
import AuthenticationServices
import SwiftUI
struct SIWAButton: View {
let onComplete: (ASAuthorizationAppleIDCredential) -> Void
let onError: (Error) -> Void
var body: some View {
SignInWithAppleButton(.signIn) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
switch result {
case .success(let auth):
guard let cred = auth.credential as? ASAuthorizationAppleIDCredential else { return }
onComplete(cred)
case .failure(let error):
onError(error)
}
}
.signInWithAppleButtonStyle(.black)
.frame(height: 48)
.cornerRadius(8)
}
}
Handle the credential
struct LoginView: View {
@State private var session: UserSession?
var body: some View {
SIWAButton(
onComplete: handle(credential:),
onError: { print("SIWA error: \($0)") }
)
}
func handle(credential: ASAuthorizationAppleIDCredential) {
// First sign-in only:
let firstName = credential.fullName?.givenName
let lastName = credential.fullName?.familyName
let email = credential.email
// Always present:
let userID = credential.user // Stable ID
let identityToken = credential.identityToken // JWT bytes
let authorizationCode = credential.authorizationCode // One-time code
Task {
// POST identityToken to your server for verification + session creation
session = try? await AuthAPI.shared.signIn(
identityToken: identityToken,
authorizationCode: authorizationCode,
firstSignInName: PersonName(firstName: firstName, lastName: lastName),
firstSignInEmail: email
)
}
}
}
Server-side verification
Your server must:
- Decode the JWT (header + payload).
- Verify the signature against Apple’s public keys (rotated; cache and refetch from
https://appleid.apple.com/auth/keys). - Verify the
issishttps://appleid.apple.com. - Verify the
audmatches your bundle identifier. - Verify
expis in the future. - Use
sub(the stable user ID) to look up or create your account.
Optionally exchange the authorizationCode for a refresh token:
POST https://appleid.apple.com/auth/token
client_id=your.bundle.id
client_secret=<JWT_signed_with_p8>
code=<authorizationCode>
grant_type=authorization_code
The client_secret is itself a JWT you sign with your Apple .p8 (Sign in with Apple key downloaded from Developer Portal). The refresh token lets your server validate the user is still active over time without re-prompting.
Credential state on app launch
When the app launches, check whether the user is still signed in (they may have revoked credentials in iOS Settings → Apple ID → Sign-In & Security → “Apps Using Apple ID”):
@MainActor
func checkSignInStatus() async {
guard let storedUserID = UserDefaults.standard.string(forKey: "siwa.userID") else { return }
let provider = ASAuthorizationAppleIDProvider()
let state = try? await provider.credentialState(forUserID: storedUserID)
switch state {
case .authorized: // Still good
break
case .revoked, .notFound:
// User signed out via system settings — clear local session, return to login
await session.signOut()
case .transferred:
// App was transferred between teams (rare) — re-auth required
break
default: break
}
}
Listen for revocation in real-time
let center = NotificationCenter.default
center.addObserver(forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil, queue: .main) { _ in
Task { await sessionStore.signOut() }
}
Re-authentication (“Sign In with existing Apple ID”)
If you want a silent check on a screen (e.g., paywall), use a quiet variant:
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.performRequests(options: .preferImmediatelyAvailableCredentials)
With .preferImmediatelyAvailableCredentials, the system uses an existing token if available without showing UI.
Web / Android — Sign in with Apple JS
Add the Sign in with Apple JS button to your web app. Configure a Services ID in the Apple Developer Portal (separate from your bundle ID, with web domains + redirect URLs). The JS button posts the same identity token to your server, where the same verification code path applies. Result: web/Android users land on the same sub as the iOS user.
App Transfer caveat
If you ever transfer your app between Apple Developer teams, all existing SIWA users will get .transferred credential state on next check. You must perform a one-time team transfer migration: download the Apple team-transfer token, POST to Apple’s migration endpoint, and Apple gives you the mapping from old sub to new sub. Don’t transfer your app without budgeting for this work.
In the wild
- Spotify, Airbnb, Twitter / X — all support SIWA alongside other providers.
- Substack, Medium — popular SIWA users in the publishing space.
- WhatsApp, Telegram — don’t use SIWA (phone-number based).
- Dropbox, Notion — full SIWA + email relay.
- Apple’s own services (App Store, Apple Music, Apple TV+) — naturally.
Common misconceptions
- “You get the email every sign-in.” No — only the first. Persist it on first contact or you’ll lose it.
- “The
useridentifier is global.” No — it’s stable per (Apple ID, app’s team). Two different apps from the same Apple ID get the sameuseronly if they’re under the same Developer Team. - “You don’t need a server for SIWA.” You can technically use it for local-only auth (just store the
user), but for any account that survives app deletion or syncs across devices, server-side verification is required. - “
credential.emailis the user’s real email.” It may be thexxx@privaterelay.appleid.comproxy if the user chose “Hide My Email.” Treat both the same way; never assume you can correlate to a “real” address. - “SIWA is required for all apps.” Only required if you use another third-party auth (Google, Facebook, etc.). Pure email/password is OK, as is no auth at all.
Seasoned engineer’s take
Sign in with Apple is one of those rare APIs where Apple did the integration work right: the button is system-styled, the privacy story is clean, the server-side primitives are standard JWT. The mistakes teams make are universally on the persistence and lifecycle side, not the API side:
- Persist the name and email on first sign-in or never see them again. Build the server endpoint to accept them and store on the user row.
- Check
credentialStateon app launch. If the user revoked you in iOS Settings, you must sign them out client-side. Apps that don’t do this present a broken “I’m logged in but every API call returns 401” state. - Subscribe to
credentialRevokedNotification. For immediate sign-out when revocation happens mid-session. - Don’t email the
xxx@privaterelay.appleid.comaddress from a domain you haven’t registered with Apple Mail Service. Apple requires you to register your sending domain via the developer portal or relay emails will bounce.
For greenfield apps: make SIWA the default auth choice. Conversion data across multiple apps consistently shows SIWA outperforming email/password and Google for users on iOS. Combine it with passkeys (Chapter 9.6) for users without an Apple ID, and you have a no-password app.
TIP: During development, you can revoke your own dev app from iOS Settings → Apple ID → Sign-In & Security → Apps Using Apple ID. Useful for testing the
.revokedcredential state path without making 50 test accounts.
WARNING: If you store the
identityTokenon disk in plain text, you’ve stored a signed assertion that anyone with the file can use to impersonate the user to your server (until expiry, typically ~10 min). Either don’t store it, or store it in Keychain.
Interview corner
Junior: “How do you add a Sign in with Apple button to a SwiftUI screen?”
Add the
AuthenticationServicesimport. UseSignInWithAppleButton(.signIn)withrequestedScopes = [.fullName, .email]in the request closure. In the completion closure, downcastauth.credentialtoASAuthorizationAppleIDCredentialand readuser,identityToken,authorizationCode, optionallyfullName/
Mid: “How do you handle the user revoking your app’s Apple ID access from iOS Settings?”
Two layers. (1) On every app foreground / launch, call
ASAuthorizationAppleIDProvider().credentialState(forUserID: storedUserID). If it returns.revokedor.notFound, sign the user out locally. (2) Subscribe toASAuthorizationAppleIDProvider.credentialRevokedNotificationfor real-time revocation. Both layers point to the samesignOut()function that clears Keychain + local DB and returns the user to the login screen.
Senior: “Design the server-side validation flow for a SIWA-enabled app, including refresh tokens and a Sign in with Apple JS web counterpart.”
Single
/auth/appleendpoint acceptsidentityTokenand (first sign-in)authorizationCode. Server: (1) Parse JWT header to getkid; fetch Apple’s JWKS fromappleid.apple.com/auth/keys(cache 24h, refresh on cache miss). Verify signature with the matching key. (2) Verifyiss,aud(must match bundle ID for native, Services ID for web), andexp. (3) Usesubas the stable user identifier. Upsert user; on insert, persist any provided email/name. (4) IfauthorizationCodepresent, sign a client-secret JWT with your.p8key (5–6-month expiry on the secret JWT), POST toappleid.apple.com/auth/tokento exchange for a refresh token. Store refresh token encrypted at rest. (5) Issue your own server session (JWT or session cookie). For periodic re-validation, hit/auth/tokenwith the stored refresh token to confirm the Apple side is still valid; if it returns an error, revoke the user’s server session. Web counterpart: Configure a Services ID with allowed domains + redirect URLs; the Sign in with Apple JS button posts the same identity token. Same/auth/appleendpoint, same verification code path — only difference is theaudclaim being the Services ID instead of bundle ID, which the server validates against an allow-list.
Red flag: “We just trust credential.email and use it as the primary key for the user account.”
Three problems: (1) On second sign-in the email is nil. (2) The email may be a relay address (changes if the user re-creates the relay). (3) The user might use SIWA on iOS and email/password elsewhere — multiple “primary keys” for the same person. Always key on
sub(theuseridentifier), treat email as a profile field.
Lab preview
Lab 7.4 — Sign in with Apple auth flow builds the complete pipeline: button → token capture → mock server verification → Keychain session → credential-state re-check → revocation handling. Pair with Chapter 9 (Security) for the production version with real JWT validation and refresh tokens.