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.

ContextWhat 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:

  1. Decode the JWT (header + payload).
  2. Verify the signature against Apple’s public keys (rotated; cache and refetch from https://appleid.apple.com/auth/keys).
  3. Verify the iss is https://appleid.apple.com.
  4. Verify the aud matches your bundle identifier.
  5. Verify exp is in the future.
  6. 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, Telegramdon’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

  1. “You get the email every sign-in.” No — only the first. Persist it on first contact or you’ll lose it.
  2. “The user identifier is global.” No — it’s stable per (Apple ID, app’s team). Two different apps from the same Apple ID get the same user only if they’re under the same Developer Team.
  3. “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.
  4. credential.email is the user’s real email.” It may be the xxx@privaterelay.appleid.com proxy if the user chose “Hide My Email.” Treat both the same way; never assume you can correlate to a “real” address.
  5. “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:

  1. 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.
  2. Check credentialState on 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.
  3. Subscribe to credentialRevokedNotification. For immediate sign-out when revocation happens mid-session.
  4. Don’t email the xxx@privaterelay.appleid.com address 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 .revoked credential state path without making 50 test accounts.

WARNING: If you store the identityToken on 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 AuthenticationServices import. Use SignInWithAppleButton(.signIn) with requestedScopes = [.fullName, .email] in the request closure. In the completion closure, downcast auth.credential to ASAuthorizationAppleIDCredential and read user, identityToken, authorizationCode, optionally fullName / email on first sign-in.

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 .revoked or .notFound, sign the user out locally. (2) Subscribe to ASAuthorizationAppleIDProvider.credentialRevokedNotification for real-time revocation. Both layers point to the same signOut() 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/apple endpoint accepts identityToken and (first sign-in) authorizationCode. Server: (1) Parse JWT header to get kid; fetch Apple’s JWKS from appleid.apple.com/auth/keys (cache 24h, refresh on cache miss). Verify signature with the matching key. (2) Verify iss, aud (must match bundle ID for native, Services ID for web), and exp. (3) Use sub as the stable user identifier. Upsert user; on insert, persist any provided email/name. (4) If authorizationCode present, sign a client-secret JWT with your .p8 key (5–6-month expiry on the secret JWT), POST to appleid.apple.com/auth/token to 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/token with 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/apple endpoint, same verification code path — only difference is the aud claim 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 (the user identifier), 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.


Next: Lab 7.1 — Weather + Map app