9 min left
0% complete
Implementing Apple Sign In: A Unified Authentication Flow for Mobile and Web
In today’s multi-platform world, offering a seamless login experience across devices is essential — but managing separate authentication flows for mobile and web can quickly become complex and error-p
In today’s multi-platform world, offering a seamless login experience across devices is essential — but managing separate authentication flows for mobile and web can quickly become complex and error-prone. One elegant solution? A unified Apple Sign In backend that handles both mobile (iOS/Android) and web clients with a single endpoint.
In this guide, you'll learn how to implement Apple Sign In using a shared API endpoint, reduce code duplication, and ensure consistent user experiences across all platforms. You’ll build a secure, scalable flow that handles token validation, user creation, cookie setup (for web), and new-user onboarding — all while adhering to Apple’s security requirements.
By the end, you’ll have a production-ready architecture that supports:
- Mobile apps using native Apple SDKs
- Web applications using Apple’s JavaScript SDK
- Server-side rendered pages requiring auth cookies
- New user registration with terms acceptance
- Robust error handling and debugging tips
Let’s dive in.
Why Apple Sign In?
Apple Sign In is more than just a login option — it's a privacy-focused identity provider that:
- Requires minimal user input (often just one tap)
- Protects user privacy with anonymized email relay
- Is required by Apple for apps on the App Store that offer third-party sign-ins
- Provides a trusted, native experience on iOS and macOS
But unlike other providers, Apple’s flow differs slightly between platforms:
- Mobile uses the native SDK (Face ID/Touch ID integration)
- Web uses a popup-based JavaScript SDK
This means developers often end up writing two different backend flows — unless they unify the logic.
Unified Architecture: One Endpoint to Rule Them All
Instead of maintaining separate endpoints for mobile and web, we use a single unified endpoint:
POST /api/v1/auth/appleThis endpoint accepts an id_token from Apple — whether it comes from a mobile app or a browser — and handles everything server-side:
- Validating the JWT token
- Finding or creating the user
- Returning standard authentication tokens
- Handling edge cases like first-time users needing to accept terms
flowchart TD
A[Mobile App<br><small>(iOS/Expo)</small>] -->|id_token| C[/api/v1/auth/apple]
B[Web Browser<br><small>(Apple JS SDK)</small>] -->|id_token| C[/api/v1/auth/apple]
C --> D{Valid Token?}
D -->|Yes| E[Find/Create User]
E --> F{New User?}
F -->|Yes| G[Return needs_terms_approval]
F -->|No| H[Return JWT Tokens]
G --> I[Client shows terms modal]
I --> J[Resend with policiesAgreement: true]
J --> EThis unified approach eliminates platform-specific logic in your backend and ensures consistent behavior, error handling, and user management.
💡 Why This Matters: You avoid writing and maintaining two nearly identical flows. One service handles all Apple logins, reducing bugs and deployment complexity.
Step 1: Create the Main Authentication Endpoint
All Apple logins hit this single endpoint.
Request Endpoint
POST /api/v1/auth/appleRequest Body
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"provider": "Apple",
"fullName": "John Doe",
"policiesAgreement": false
}| Field | Description |
|---|---|
idToken | JWT token returned by Apple SDK (required) |
provider | Optional, defaults to "Apple" |
fullName | Available only on first sign-in, sent by Apple |
policiesAgreement | true when user accepts terms |
Success Response (User Authenticated)
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "abc123...",
"accessTokenExpiresInSeconds": 900,
"refreshTokenExpiresAtUtc": "2024-02-12T12:00:00Z",
"user": {
"id": "user-guid",
"email": "user@icloud.com",
"name": "John Doe",
"userName": "user@icloud.com",
"bio": null,
"shortId": "abc123",
"isInfluencer": false,
"avatar": null
}
}Pending Terms Response (New User)
If this is the first time the user signs in, Apple only gives basic info, and your app must prompt the user to accept terms:
{
"status": "needs_terms_approval",
"prefill": {
"email": "user@icloud.com",
"usernameSuggestion": "user",
"name": "John Doe",
"avatarUrl": null
}
}🔐 The client should store theidTokenand show a modal asking the user to agree to terms. When confirmed, resend the same request with"policiesAgreement": true.
Step 2: Token Validation – The Heart of Security
Never trust the id_token at face value. Validate it thoroughly using Apple’s public keys.
Here’s what your server must do:
- Fetch Apple's public keys from:
`
https://appleid.apple.com/auth/keys
`
These are published in JWKS format.
- Verify the JWT signature using the matching RSA public key (from
kidclaim). - Validate claims:
- iss (issuer) must be https://appleid.apple.com
- aud (audience) must match one of your allowed client IDs
- exp (expiration) must not be in the past
- sub (subject) uniquely identifies the user
Example Validation Snippet (Pseudocode)
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = "https://appleid.apple.com",
ValidAudiences = new[] { "com.yourapp.mobile", "com.yourapp.web", "host.exp.Exponent" },
IssuerSigningKeys = applePublicKeys // Loaded from JWKS
};
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(idToken, validationParameters, out validatedToken);💡 Always use a library likeSystem.IdentityModel.Tokens.Jwt(C#) orjose(Node.js) to handle JWT parsing and validation securely.
Step 3: Configuration – Setup Your App in Apple’s Ecosystem
Before you can accept Apple sign-ins, you must configure your app in the Apple Developer Console.
Required Components
| Component | Purpose |
|---|---|
| App ID | For iOS/Android apps; enables Apple Sign In in native SDKs |
| Services ID | For web login; acts as the client ID in the JS SDK |
| Private Key (.p8) | Used if doing server-to-server flows (optional here) |
appsettings.json or .env Configuration
{
"AppleAuth": {
"ClientIds": [
"com.yourapp.mobile",
"com.yourapp.mobile.dev",
"com.yourapp.web",
"host.exp.Exponent"
],
"OAuthClientId": "com.yourapp.web",
"OAuthRedirectUri": "https://your-domain.com/api/v1/oauth/apple/callback"
}
}| Setting | Description |
|---|---|
ClientIds | List of all valid bundle IDs and Services IDs (used to validate aud) |
OAuthClientId | Web Services ID (used in JS SDK) |
OAuthRedirectUri | Must match exactly in Apple Developer Console |
🔄 Tip: Use the same ClientIds array in both configuration and token validation to avoid mismatches.Step 4: Web-Only – Setting Authentication Cookies
Mobile apps can store JWTs in secure storage (like Keychain or SecureStore). But web apps often need server-side authentication cookies, especially for server-rendered pages (e.g., ASP.NET, Rails, Laravel).
That’s where a second endpoint comes in:
Cookie Setup Endpoint
POST /api/v1/auth/apple-web-loginThis endpoint:
- Accepts the
id_token - Verifies it (again, for security)
- Creates a secure, HTTP-only cookie using your framework’s identity system
Request Body
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response
{
"success": true
}⚠️ This endpoint should only be called after the main /auth/apple endpoint succeeds. It’s a follow-up to enable cookie-based auth.When Is This Needed?
| Platform | Uses JWT? | Uses Cookie? |
|---|---|---|
| Mobile | ✅ Yes | ❌ No |
| Web (SPA) | ✅ Yes (in memory) | ❌ Optional |
| Web (Server-rendered) | ✅ Optional | ✅ Yes |
For SPAs using client-side rendering, JWTs alone may suffice. But for SSR apps (like traditional MVC), cookies are necessary to maintain login state across page refreshes.
Step 5: Frontend Implementation – Web (JavaScript SDK)
On the web, use Apple’s JavaScript SDK with popup mode to avoid redirecting the user.
Initialize the SDK
AppleID.auth.init({
clientId: 'com.yourapp.web',
scope: 'name email',
redirectURI: 'https://your-domain.com/api/v1/oauth/apple/callback',
usePopup: true
});clientId: Your Services ID from Applescope: Request user’s name and emailredirectURI: Must match Apple console exactlyusePopup: true: Prevents full-page redirect
Trigger Sign In
AppleID.auth.signIn().then(function(response) {
const idToken = response.authorization.id_token;
// Send to your unified backend
fetch('/api/v1/auth/apple', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken, fullName: 'John Doe' })
})
.then(r => r.json())
.then(data => {
if (data.status === 'needs_terms_approval') {
showTermsModal(data.prefill, idToken);
} else {
handleSuccessfulLogin(data);
}
});
});💡 You can extract the user’s name from theid_token'snamefield the first time Apple returns it (after that, it'snullfor privacy).
Step 6: Handling New Users – Terms Acceptance Flow
Apple may not give you full consent on the first login. Here's how to handle it:
Step-by-Step Flow
- User signs in via Apple.
- Server validates token, sees no existing user.
- Returns
{ status: "needs_terms_approval" }. - Client shows a modal with pre-filled email/name.
- User checks "I agree to the Terms of Service."
- Client sends the same `id_token` with
policiesAgreement: true. - Server creates the user account.
- Returns full JWT tokens.
Resending After Approval
function submitAfterTerms(idToken) {
fetch('/api/v1/auth/apple', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken, policiesAgreement: true })
})
.then(r => r.json())
.then(data => handleSuccessfulLogin(data));
}⏳ Token Expiry: Apple’s id_token is short-lived (usually 5 minutes). If the user takes too long in the modal, the token may expire. Catch the error and prompt re-authentication.Security Best Practices
- Validate tokens server-side only
Never assume the frontend has validated anything.
- Use HTTPS everywhere
Apple requires all redirect URIs to use HTTPS. No exceptions.
- Store the `sub` claim as external ID
Apple assigns a unique sub (subject) per user per app. Use this to link Apple accounts to your users.
- Set secure cookie flags
For /apple-web-login, ensure cookies have:
- HttpOnly: Prevents JS access
- Secure: Only sent over HTTPS
- SameSite=Strict or Lax: Prevents CSRF
- Limit client IDs
Only allow known IDs in your ClientIds list. Reject tokens meant for other apps.
Testing Apple Sign In
Local Development Gotcha
Apple Sign In does not work on `http://localhost` due to domain and HTTPS restrictions.
Solutions:
- Use `ngrok`: Create a secure tunnel to your local server
ngrok http https://localhost:5001 → https://abc123.ngrok.io
- Register the ngrok URL in Apple Developer Console under your Services ID
- Test on staging if ngrok isn’t viable
Apple Sandbox Accounts
Use Apple's Sandbox Test Users in App Store Connect:
- These are real test Apple IDs
- Can be used to test sign-in without affecting production users
- No real payment methods attached
Common Issues & Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| "Invalid redirect URI" | Mismatch with Apple Console | Double-check casing and HTTPS |
| "Invalid Apple token" | Wrong aud or expired token | Check ClientIds and token expiry |
| No email provided | User hid email | Guide user to revoke access in Settings and retry |
| CORS errors | Frontend/backend domains differ | Set proper CORS headers in API |
| Cookie not set | Missing withCredentials: true | Ensure fetch includes credentials |
🛠️ Check browser dev tools and server logs. Most issues stem from domain misconfiguration or token validation failures.
Summary: Key Takeaways
You’ve now built a unified, secure, and maintainable Apple Sign In system that works across platforms.
Core Principles
✅ One Endpoint – /api/v1/auth/apple handles mobile and web
✅ Shared Validation – Same token logic for all clients
✅ Web Cookie Support – Optional second call for SSR apps
✅ New User Flow – Clean terms approval with prefilled data
✅ Configurable Client IDs – Support staging, prod, and dev apps
Why This Architecture Wins
- Reduces code duplication
- Simplifies updates (e.g., token validation changes)
- Ensures consistent error handling
- Scales to new platforms easily (e.g., desktop, TV)
Apple Sign In doesn’t have to be complicated. With a unified backend, you deliver a seamless experience — whether the user is on iPhone, Android, or their browser.
Now go ship it. 🚀