Apple Sign In is not optional — if your app offers any third-party login (Google, Facebook, etc.), Apple mandates you also offer Sign In with Apple or it gets rejected from the App Store. I learned this the hard way during a review cycle on a client project. This post documents every step from Xcode capability setup to credential state monitoring, plus the often-ignored Android fallback that most guides skip entirely.
Why Sign In with Apple is different
Unlike Google or Facebook OAuth, Apple Sign In has a few unique behaviours you must design around: Apple only sends the user's name and email on the very first login. If the user revokes and re-authorises, you get the user ID but not the name/email again — ever. You must persist name and email immediately on first sign-in, not on a subsequent profile fetch. Apple also offers a private email relay (e.g. abc123@privaterelay.appleid.com) that forwards to the user's real inbox. Your backend must be whitelisted to send to these relay addresses via Apple's developer portal.
Step 1 — Xcode capability and Apple Developer setup
In Xcode, open your project → select the target → go to Signing & Capabilities → click the + button → add Sign In with Apple. This creates an entitlement file automatically. In the Apple Developer portal, go to Identifiers → select your App ID → enable Sign In with Apple → configure the primary App ID if you're using an extension. For backend token validation, create a Services ID (used as the client_id for web flows) and generate a Key (.p8 file) under Keys — this is your client secret material for the REST API.
Step 2 — Install the library
npm install @invertase/react-native-apple-authentication # iOS cd ios && pod install # No additional linking needed for RN 0.63+
Step 3 — Trigger the Sign In flow
import appleAuth, {
AppleButton,
AppleAuthRequestOperation,
AppleAuthRequestScope,
AppleAuthCredentialState,
} from '@invertase/react-native-apple-authentication';
import { Alert } from 'react-native';
export default function AppleSignInButton() {
async function handleAppleSignIn() {
try {
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.LOGIN,
requestedScopes: [
AppleAuthRequestScope.EMAIL,
AppleAuthRequestScope.FULL_NAME,
],
});
const { user, email, fullName, identityToken, nonce } =
appleAuthRequestResponse;
// CRITICAL: persist name/email immediately — Apple won't send again
const displayName =
fullName?.givenName && fullName?.familyName
? `${fullName.givenName} ${fullName.familyName}`
: 'Apple User';
// Send identityToken + nonce to your backend for JWT verification
await sendToBackend({ identityToken, nonce, user, email, displayName });
} catch (error) {
if (error.code === '1001') return; // user cancelled — not an error
Alert.alert('Sign In Failed', error.message);
}
}
if (!appleAuth.isSupported) return null;
return (
<AppleButton
buttonStyle={AppleButton.Style.WHITE}
buttonType={AppleButton.Type.SIGN_IN}
style={{ width: 200, height: 44 }}
onPress={handleAppleSignIn}
/>
);
}Step 4 — Backend JWT verification
Never trust the identityToken on the client side alone. Send the identityToken and nonce to your server. On the backend, fetch Apple's public keys from https://appleid.apple.com/auth/keys, verify the JWT signature using the matching key, check the nonce matches, confirm the iss is https://appleid.apple.com, and confirm the aud matches your Bundle ID. Libraries like apple-signin-auth (Node.js) or python-jose handle this cleanly. Only after verification create or update the user record.
Step 5 — Monitor credential state changes
import { useEffect } from 'react';
import appleAuth, {
AppleAuthCredentialState,
} from '@invertase/react-native-apple-authentication';
import { useDispatch } from 'react-redux';
import { signOut } from '../store/authSlice';
export function useAppleCredentialListener(userId) {
const dispatch = useDispatch();
useEffect(() => {
if (!userId || !appleAuth.isSupported) return;
// Fires when user revokes Apple Sign In from iOS Settings
const unsubscribe = appleAuth.onCredentialRevoked(async () => {
const state = await appleAuth.getCredentialStateForUser(userId);
if (state === AppleAuthCredentialState.REVOKED) {
dispatch(signOut());
}
});
return () => unsubscribe();
}, [userId]);
}Android — the forgotten step
Apple Sign In on Android requires a web-based OAuth flow through a Services ID (not the App ID). You redirect the user to Apple's auth URL in a WebView or browser, receive a code + id_token via your redirect URI (must be HTTPS), and exchange it on your backend. The @invertase library supports this via appleAuthAndroid module. Key difference: you must generate and verify a nonce client-side as a SHA256 hash, pass it raw to Apple, and verify the hashed version in the JWT. Skipping the nonce check is a security hole.
import { appleAuthAndroid } from '@invertase/react-native-apple-authentication';
import 'react-native-get-random-values';
import { v4 as uuid } from 'uuid';
import SHA256 from 'crypto-js/sha256';
async function handleAndroidAppleSignIn() {
const rawNonce = uuid();
const state = uuid();
appleAuthAndroid.configure({
clientId: 'com.yourcompany.yourapp.service', // Services ID
redirectUri: 'https://yourapp.com/auth/apple/callback',
responseType: appleAuthAndroid.ResponseType.ALL,
responseMode: appleAuthAndroid.ResponseMode.QUERY,
scope: appleAuthAndroid.Scope.ALL,
nonce: rawNonce,
state,
});
const response = await appleAuthAndroid.signIn();
// Send response.id_token + rawNonce to backend for verification
}Three things that will get your app rejected: not offering Apple Sign In when you offer other social logins, not handling the credential revocation listener, and sending marketing emails to Apple's private relay without whitelisting your domain in the Apple Developer portal. Check all three before submitting.