Adding Sign in with Apple to Your Expo App: the Short Answer
Adding Sign in with Apple to your Expo app is four steps: install expo-apple-authentication, enable the Apple authentication capability so EAS Build provisions it, render Apple's own native button, and handle the credential the OS hands back when the user completes the flow. Expo wraps Apple's AuthenticationServices framework behind a small JavaScript module, so from your side it is one install, one config entry, one button component, and one async call. The button only appears on iOS, which is correct - Sign in with Apple is an iOS and iPadOS capability, and your Android build uses your other login methods.
The part that trips up first-time builders is not the button. It is what happens after the tap. Apple returns the user's full name and email exactly once - on the first sign-in - and returns a stable user identifier every time. If you do not persist the name and email on that first callback, they are gone, and you are left with an account that has an ID but no way to greet the person or send them anything. Get the first-sign-in capture right and the rest of the flow is standard token handling.
Step 1 - Install the Module and Enable the Capability
Install the module the Expo way so its config plugin is registered: run npx expo install expo-apple-authentication rather than a plain npm install. The expo install command pins a version compatible with your SDK and wires the config plugin into your build, which is what adds Apple's Sign in with Apple entitlement to the native project during EAS Build. Without the entitlement, the button renders but signInAsync() fails on a real device.
- Run npx expo install expo-apple-authentication to add the module and register its config plugin.
- Add 'expo-apple-authentication' to the plugins array in app.json (the module's docs list the exact plugin entry), or confirm expo install added it for you.
- Set ios.usesAppleSignIn to true in app.json. This is the field that tells EAS Build to enable the Sign in with Apple capability on the app identifier.
- Run a fresh development build or EAS Build after this change. The capability is baked into the native app, so it does not take effect on a JavaScript reload or in a generic Expo Go session.
Step 3 - Handle the Credential You Get Back
When the user completes the flow, signInAsync() resolves with a credential object. The two fields that matter for your account system are the stable user identifier, which is the same on every sign-in and is how you recognize a returning user, and the identityToken, a signed JWT you send to your backend to verify the sign-in and mint your own session. The fullName and email fields are the ones with the catch: Apple populates them only on the first authorization.
What each field in the Apple credential is for
| Field | When it is present | What you do with it |
|---|---|---|
| user | Every sign-in (stable per app per person) | Your primary key for the account - look up or create the user by this ID |
| identityToken | Every sign-in | Send to your backend to verify with Apple's public keys and create your session |
| fullName | First sign-in only | Persist it the first time; it is null on every later sign-in |
| First sign-in only | Persist it the first time; may be a private relay address the user chose to share | |
| authorizationCode | Every sign-in | Send to your backend if you need refresh tokens for server-side session revocation |
The verification belongs on your backend, not in the app. Your app sends the identityToken up; your server checks the token's signature against Apple's public keys, confirms it was issued for your app, and only then creates a session. Trusting the raw credential on the device alone means anyone who can call your API can claim to be any user. Treat the app side as collecting the token and the server side as the source of truth.
Capture the Name and Email on First Sign-In
This is the single most common Sign in with Apple bug, and it is silent - everything works in testing until a real user signs in a second time. Apple returns fullName and email only on the very first authorization for your app. On every subsequent sign-in those fields are null, by design, because Apple assumes you saved them. If your account creation code only runs after the first sign-in has already happened once, you never see the name or email at all.
- On the credential callback, check whether fullName and email are present. If they are, this is a first sign-in - send them to your backend and store them on the account immediately.
- Key everything off the stable user identifier, not the email. The user ID never changes; the email can be a private relay and the user can stop sharing it later.
- To test this repeatedly during development, revoke your app's access under the Apple ID settings (Sign in with Apple section) on the test device. That resets the first-sign-in state so Apple sends the name and email again on the next attempt.
- Never require the email to be a real, deliverable address you can market to. A user who picks the private relay option gets an anonymized forwarding address, and forcing them off it is against the rules and a rejection risk.
Let Claude Code Wire the Whole Flow
The Sign in with Apple flow is mechanical and has a fixed shape, which is exactly the kind of work Claude Code handles well from the desktop app. Describe what you want - 'add Sign in with Apple to my login screen and create a session on my backend' - and it writes the availability check, the button render, the signInAsync call, the first-sign-in capture, and the backend token exchange. It also knows the app.json fields the capability needs, so it can add usesAppleSignIn and the plugin entry for you.
- Have Claude Code add the module, the plugin entry, and ios.usesAppleSignIn to app.json in one pass, so the capability is wired before you build.
- Ask it to generate the login button behind an isAvailableAsync() guard, with the correct Apple button type and a black-or-white style matched to your screen.
- Tell it to write the first-sign-in capture explicitly - the branch that persists fullName and email only when they are present - because that is the piece a hand-written flow most often gets wrong.
- Ask for the backend verification stub too: the endpoint that takes the identityToken, verifies it against Apple's public keys, and returns your session. Keep verification on the server, and let Claude Code scaffold both halves at once.
Common Sign in with Apple Mistakes and How to Avoid Them
Sign in with Apple bugs cluster around a few predictable mistakes. Most come from missing the capability, forgetting the first-sign-in data capture, or verifying the token in the wrong place.
Common Sign in with Apple mistakes in an Expo app
| Mistake | What goes wrong | Fix |
|---|---|---|
| Not setting ios.usesAppleSignIn in app.json | The button renders but signInAsync() fails with an authorization error on device | Set usesAppleSignIn to true and run a fresh EAS Build so the entitlement is provisioned |
| Reading fullName and email on every sign-in | They are null after the first sign-in, so accounts end up with no name or email | Persist them on the first callback only, keyed off the stable user identifier |
| Trusting the credential on the device | Anyone who can call your API can impersonate any user | Send the identityToken to your backend and verify it against Apple's public keys before creating a session |
| Rendering the Apple button on unsupported devices | A button that taps into nothing on Android or old iOS | Guard the render with AppleAuthentication.isAvailableAsync() |
| Adding Google login without Apple | Guideline 4.8 rejection for offering a third-party login with no privacy-preserving equivalent | Offer Sign in with Apple alongside any third-party or social login |
TL;DR
Add Sign in with Apple to your Expo app by installing expo-apple-authentication, setting ios.usesAppleSignIn to true in app.json, rendering AppleAuthentication.AppleAuthenticationButton behind an isAvailableAsync() guard, and calling signInAsync() on tap. Send the identityToken to your backend and verify it against Apple's public keys before you create a session - never trust the credential on the device alone. Capture fullName and email on the first sign-in only, because Apple returns null for both on every later sign-in, and key your accounts off the stable user identifier rather than the email. If your app offers any third-party login, Guideline 4.8 requires this option too. Claude Code wires the button, the capture, and the backend exchange in one pass; you confirm the first-sign-in branch by hand, because that is the one step with no second chance.
Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.
Frequently asked questions
Do I have to add Sign in with Apple to my Expo app?
Only if your app offers another third-party or social login. Guideline 4.8 requires that any app using a third-party login service (like Google or Facebook) also offers a login option that limits data collection to name and email and lets the user keep their email private. Sign in with Apple meets that bar. If your app only has its own email-and-password login, you are not required to add it, though many builders do anyway because iOS users trust it.
Why is the email null after the first Sign in with Apple sign-in?
By design. Apple returns the user's fullName and email only on the very first authorization for your app and assumes you saved them. Every later sign-in returns null for both and only gives you the stable user identifier. Persist the name and email on that first callback. To test again during development, revoke your app under the Apple ID Sign in with Apple settings on the device, which resets the first-sign-in state.
Where should I verify the Apple identity token?
On your backend, never in the app alone. Your app collects the identityToken from the credential and sends it to your server, which verifies the signature against Apple's public keys, confirms it was issued for your app, and only then creates a session. Trusting the credential on the device means anyone who can call your API can impersonate a user. The app collects; the server is the source of truth.
Does Sign in with Apple work on Android in an Expo app?
The native button is an iOS and iPadOS capability, so AppleAuthentication.isAvailableAsync() returns false on Android and the button does not render there. Guard your login screen with that availability check and fall back to your other login methods on Android. You build both platforms from one Expo codebase, so the same login screen shows Apple on iOS and your other options everywhere else.
Can I style the Sign in with Apple button myself?
No. Apple requires the standard button provided through AppleAuthentication.AppleAuthenticationButton so it looks the same across every app. You can choose the button type (sign in, continue, sign up), the style (black, white, or white with an outline), and the corner radius, and you should match black or white to your screen background, but you cannot replace it with a custom-drawn button.
Do I need EAS Build to test Sign in with Apple?
You need a build that includes the Sign in with Apple entitlement, which a development build or EAS Build with ios.usesAppleSignIn set to true provides. The generic Expo Go app does not carry your app's entitlement, so the sign-in call fails there. Make a development build of your own app and test the full flow on a real device before you submit.
Last reviewed by David on September 15, 2026


