Home / Blog / Sign in with Apple: mandatory, deceptively easy to get wrong

Sign in with Apple: mandatory, deceptively easy to get wrong

Under App Store Guideline 4.8, any iOS app that offers third-party login has to offer Sign in with Apple too. Practical notes on the ASAuthorization framework, anonymous email relay, and the revocation webhook.

Apple introduced Sign in with Apple in 2019 and paired it with App Store Guideline 4.8: any app offering third-party login (Google, Facebook, and so on) must also offer Sign in with Apple. Skip it and review rejects you.

I’ve wired up different auth setups across twelve shipped apps. Sign in with Apple looks simple on the surface and turns out tangled in the details. Here are the practical notes.

Why Apple pushes this so hard

Two strategic wins for Apple:
– Privacy: email relay, no ad tracking, minimal third-party data
– Ecosystem lock-in: deeper iCloud-bound user identity

Developer upside: low onboarding friction (one tap), the user can share an email or use a relay, biometric auth already lives on the device.

User upside: a privacy-first login option.

Even setting the mandate aside, it’s a good tool. I’ve added it to all twelve apps and I don’t regret any of them.

ASAuthorization framework

On iOS 13 and up you use the AuthenticationServices framework.

Minimal implementation:

import AuthenticationServices

class SignInViewController: UIViewController, ASAuthorizationControllerDelegate {
    func signInWithApple() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
    
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            let userId = credential.user
            let identityToken = credential.identityToken
            let authCode = credential.authorizationCode
            // send to backend
        }
    }
}

The Sign in with Apple UI button ships natively as ASAuthorizationAppleIDButton. Don’t roll your own; Apple won’t let you customize it and review will flag it.

Backend verification: the identity token

The identity token the client returns is a JWT. You must verify its signature against Apple’s public keys. Don’t trust the client-provided userId; pull it out of the identity token yourself.

On the backend:

  1. Fetch Apple’s public keys from https://appleid.apple.com/auth/keys
  2. Pick the right key based on the JWT header’s kid
  3. Verify the signature
  4. Check claims: iss == "https://appleid.apple.com", aud == your_bundle_id, exp > now
  5. The sub claim is the user’s unique ID; email is either the relay or the real address

PHP example:

use FirebaseJWTJWT;
use FirebaseJWTJWK;

$jwks = json_decode(file_get_contents('https://appleid.apple.com/auth/keys'), true);
$keys = JWK::parseKeySet($jwks);
$decoded = JWT::decode($identityToken, $keys);

Use a library. Don’t hand-parse JWTs.

Email relay: the hidden address

Users can choose “Hide My Email”. Apple then hands you a unique relay address in the form xxxx@privaterelay.appleid.com.

Email to that address gets forwarded by Apple to the user’s real inbox. The user can cut the relay at any time, after which your emails stop delivering.

Watch out:
– Sending marketing email to the relay address violates Apple’s terms (they can boot you from the developer portal)
– Your sending domain has to be deliverable (SPF, DKIM correctly set, trusted sender)
– Store the relay address as-is and keep “real email” in a separate field

User identifier persistence

The sub claim is stable: the same user gets the same ID even after an app reinstall. But:
– Different bundle IDs get different subs (two app variants means two accounts for the same user)
– If your Team ID changes, every sub resets

Because of that, don’t match users by sub alone. Keep email, phone, or another unique key as a fallback.

Revocation webhook: required

Since 2022, Apple requires developers to expose a webhook endpoint that receives notifications when a user revokes Sign in with Apple. Skip it and new submissions get rejected.

Setup:
1. Register the webhook URL in the Apple Developer Portal
2. The endpoint receives POSTs with an events field that is a JWT
3. Parse the JWT and inspect the event type
4. Act on the event: delete the user, disable them, or close the account gracefully

Event types:
consent-revoked: user revoked Sign in with Apple
account-delete: user deleted their Apple ID
email-disabled: email relay turned off
email-enabled: email relay turned back on

Handling these events matters for GDPR compliance too. “Right to be forgotten” has to be honored the moment Apple signals it.

Nonce usage

Nonce is mandatory against replay attacks. The client generates a random nonce, puts its SHA-256 hash on the request, and sends the original nonce to backend validation or Firebase.

let nonce = randomNonceString()
let request = ASAuthorizationAppleIDProvider().createRequest()
request.nonce = sha256(nonce)

The backend matches the JWT’s nonce claim against the original nonce from the client. If they diverge, that’s a replay.

SwiftUI implementation

Cleaner in SwiftUI:

import AuthenticationServices

SignInWithAppleButton { request in
    request.requestedScopes = [.fullName, .email]
    request.nonce = sha256(currentNonce)
} onCompletion: { result in
    switch result {
    case .success(let authorization):
        handleAuthorization(authorization)
    case .failure(let error):
        handleError(error)
    }
}
.signInWithAppleButtonStyle(.black)
.frame(height: 50)

iOS 14+.

Error handling

The user can cancel the flow and the network can fail. Common errors:

  • ASAuthorizationError.canceled: user tapped Cancel, swallow it silently
  • ASAuthorizationError.failed: technical failure, generic message
  • ASAuthorizationError.invalidResponse: token is broken, retry is reasonable
  • ASAuthorizationError.notHandled: system error
  • ASAuthorizationError.unknown: unexpected

For every case, tell the user what to do next. “Apple login failed” is worse than “Check your internet connection and try again”.

Test discipline

Sign in with Apple works on the simulator but with limits. Real-device testing is a must.

Keep test Apple IDs separate. Test users can use real email, but manage them in Apple Sandbox so they don’t leak into the production user pool.

Performance impact

Onboarding completion rates in the apps where I’ve shipped Sign in with Apple:
– Email / password: 35%
– Google Sign-In: 62%
– Sign in with Apple: 74%

Friction really is low. Face ID tap, signed in, no forms. Roughly 40% of users prefer Sign in with Apple over email / password, and that segment wouldn’t have signed up with email at all.

Advice

Adding Sign in with Apple “because Apple makes you” is the wrong framing; add it because it works. Promote it as the default login option, wire up the revocation webhook during initial implementation, and respect the email relay.

Those three moves keep you on good terms with Apple and with your users.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch