Developing Apple ID Authentication (Sign in with Apple)
Since 2019, Apple requires: if iOS app has third-party identity provider login (Google, Facebook, VK), Sign in with Apple must be present. App Store Review rejection on 4.8.0 — common story for teams that integrated Google Sign-In but ignored Apple. It's not just review requirement — it's good authentication with privacy-centric design.
What Makes Sign in with Apple Special
Apple doesn't give real user email to the app — if user chose email hiding, app gets relay address like [email protected]. Emails to this address, Apple forwards to real one. If user revokes app access — relay stops working.
Name and email passed only once — at first authorization. If you didn't save them in your database — on repeat login, Apple won't return them. This is not a bug, it's intentional Apple decision. Teams that didn't know found problem in production: users can't re-login because backend didn't save email on first auth.
Implementation on iOS
import AuthenticationServices
// Request authorization
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
// Handle result
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
let userID = credential.user // Stable user identifier
let identityToken = credential.identityToken // JWT for server verification
let authorizationCode = credential.authorizationCode // For exchanging for refresh token
// email and fullName available ONLY at first authorization
let email = credential.email
let fullName = credential.fullName
}
credential.user — stable identifier, unique for (user, app) pair. Not used for identification between one team's apps (for that — identityToken with sub claim).
Server Verification
Client passes identityToken (JWT) to backend. Server verifies:
- Token signature with Apple public key (keys at
https://appleid.apple.com/auth/keys) -
audclaim matches app Bundle ID -
iss=https://appleid.apple.com -
expnot expired
After first authorization, authorizationCode exchanged for refresh_token via Apple's token endpoint. refresh_token stored on server and used to check if user revoked app access.
Check authorization status on app launch:
let appleIDProvider = ASAuthorizationAppleIDProvider()
appleIDProvider.getCredentialState(forUserID: savedUserID) { state, error in
switch state {
case .authorized: break // All good
case .revoked: // User revoked access — logout
case .notFound: // First login or data deleted
}
}
Integration on Android and Web
Apple requires Sign in with Apple on all platforms where app exists. On Android — via OIDC flow in browser (no native SDK): open ASWebAuthenticationSession equivalent through Custom Tab, authorization via Apple's OAuth endpoint, get code in redirect URI.
Retrofit / OkHttp on Android: usual OAuth2 PKCE flow with Apple's authorization endpoint. Apple doesn't provide Android SDK — only web browser flow.
Common Mistakes
- Didn't save email on first authorization — on "account recovery" attempt user can't because email unknown
- Didn't implement revocation check — user revoked access but app stays logged in
- Didn't setup Service ID for web/Android flow —
invalid_clienterror on OAuth2
Timeline: 1 to 2 weeks. Includes native iOS implementation, server JWT verification, relay email storage, status check on launch, optionally — Android OAuth2 flow.







