0

9 min left

0% complete

Blog

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

·9 min read

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/apple

This endpoint accepts an id_token from Apple — whether it comes from a mobile app or a browser — and handles everything server-side:

  1. Validating the JWT token
  2. Finding or creating the user
  3. Returning standard authentication tokens
  4. 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 --> E

This 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/apple

Request Body

{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "provider": "Apple",
  "fullName": "John Doe",
  "policiesAgreement": false
}
FieldDescription
idTokenJWT token returned by Apple SDK (required)
providerOptional, defaults to "Apple"
fullNameAvailable only on first sign-in, sent by Apple
policiesAgreementtrue 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 the idToken and 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:

  1. Fetch Apple's public keys from:

`

https://appleid.apple.com/auth/keys

`

These are published in JWKS format.

  1. Verify the JWT signature using the matching RSA public key (from kid claim).
  2. 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 like System.IdentityModel.Tokens.Jwt (C#) or jose (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

ComponentPurpose
App IDFor iOS/Android apps; enables Apple Sign In in native SDKs
Services IDFor 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"
  }
}
SettingDescription
ClientIdsList of all valid bundle IDs and Services IDs (used to validate aud)
OAuthClientIdWeb Services ID (used in JS SDK)
OAuthRedirectUriMust 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-login

This 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?

PlatformUses 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 Apple
  • scope: Request user’s name and email
  • redirectURI: Must match Apple console exactly
  • usePopup: 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 the id_token's name field the first time Apple returns it (after that, it's null for 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

  1. User signs in via Apple.
  2. Server validates token, sees no existing user.
  3. Returns { status: "needs_terms_approval" }.
  4. Client shows a modal with pre-filled email/name.
  5. User checks "I agree to the Terms of Service."
  6. Client sends the same `id_token` with policiesAgreement: true.
  7. Server creates the user account.
  8. 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

  1. Validate tokens server-side only

Never assume the frontend has validated anything.

  1. Use HTTPS everywhere

Apple requires all redirect URIs to use HTTPS. No exceptions.

  1. 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.

  1. 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

  1. 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:5001https://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

ProblemLikely CauseFix
"Invalid redirect URI"Mismatch with Apple ConsoleDouble-check casing and HTTPS
"Invalid Apple token"Wrong aud or expired tokenCheck ClientIds and token expiry
No email providedUser hid emailGuide user to revoke access in Settings and retry
CORS errorsFrontend/backend domains differSet proper CORS headers in API
Cookie not setMissing withCredentials: trueEnsure 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. 🚀

Contents