Jonas Stamm

AI engineer, growth hacker, builder

Back to all posts
·3 min read

I Built SSO From Scratch on BauGPT Mobile. Here's Why It Took 3 Days.

build-in-public

I spent 3 days on Sign in with Apple.

Apple controls the device. The user is already logged in. Just press a button.

Should've been an afternoon.

The Setup

Building BauGPT mobile (React Native + Expo) and needed authentication without passwords. Sign in with Apple seemed obvious — it's the gold standard on iOS, required by App Store guidelines for apps with sign-up buttons.

Then I opened the docs.

Problem #1: The Documentation Gap

Apple's Sign in with Apple docs are written for web developers. HTML examples. OAuth2 flow assumed. "Team ID" mentioned casually, like it's obvious what that means.

React Native developers get less help.

I found three fragmented resources:

  1. Apple's official docs (web-focused)
  2. The react-native-apple-authentication library (outdated examples)
  3. Stack Overflow (everyone's error was different)

What I should have done: Start with the @react-native-apple-authentication package README. It actually covers the React Native-specific parts.

Problem #2: Team ID Configuration

Sign in with Apple requires a "team ID" — a 10-character code Apple assigns to your Developer Account. Where do you put it?

In React Native, it goes in XCode build settings:

DEVELOPMENT_TEAM = XXXXX1Y2Z (your team ID)

I spent an hour assuming there was a JavaScript config file. There isn't.

Found the answer buried in a GitHub issue comment:

"Put your team ID in XCode project settings under Build Settings > Code Signing Identity"

One sentence. Changed everything.

Problem #3: The Return URL Redirect

After the team ID fix, the app crashed with "Invalid redirect URI".

In web OAuth: https://yourapp.com/callback — simple.

In React Native: it's a deep link that doesn't point to a real URL.

The format is: YOUR_APP_BUNDLE_ID://

For example: com.baugpt.mobile://

Apple validates this against your bundle ID in App Store Connect. Typo? Redirect URI error. I had a typo across three different config files. Two hours to find it.

Problem #4: JWT Token Validation

After the redirect worked, I got a JWT token back from Apple. Now what?

The token contains a sub claim (unique user ID) and an email claim. You validate the JWT signature using Apple's public keys.

Most tutorials fail here: they show toy examples that skip validation entirely, or they use libraries that don't handle Apple's key format.

Apple's keys rotate. Your validation code needs to handle that. I used jsonwebtoken with manual key fetching from Apple's .well-known/jwks.json endpoint.

This added latency to every login (fetching and validating). For production, cache the keys with a 24-hour TTL.

The Solution (50 Lines)

Here's what actually works:

// React Native Sign in with Apple
import * as AppleAuthentication from 'expo-apple-authentication';
import * as SecureStore from 'expo-secure-store';

export async function signInWithApple() {
  try {
    const credential = await AppleAuthentication.signInAsync({
      requestedScopes: [
        AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
        AppleAuthentication.AppleAuthenticationScope.EMAIL,
      ],
    });

    // credential.identityToken is a JWT
    // Validate it on your backend — never trust client-side validation

    const response = await fetch('https://yourapi.com/auth/apple', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        identityToken: credential.identityToken,
        user: credential.fullName, // May be null on subsequent logins
      }),
    });

    const { sessionToken } = await response.json();
    await SecureStore.setItemAsync('sessionToken', sessionToken);
    
    return true;
  } catch (error) {
    if (error.code === 'ERR_CANCELLED') {
      return false;
    }
    throw error;
  }
}

The backend does the real work:

# Python/FastAPI backend
from jose import jwt
import requests

@app.post("/auth/apple")
async def auth_apple(request: AppleAuthRequest):
    keys_response = requests.get("https://appleid.apple.com/auth/keys")
    keys = {key['kid']: key for key in keys_response.json()['keys']}
    
    try:
        payload = jwt.decode(
            request.identityToken,
            keys,
            algorithms=["ES256"],
            audience="YOUR_BUNDLE_ID",
            issuer="https://appleid.apple.com"
        )
    except Exception as e:
        raise HTTPException(status_code=401, detail="Invalid token")
    
    user_id = payload['sub']
    email = payload.get('email')
    
    user = await get_or_create_user(user_id, email)
    session = create_session(user)
    
    return {"sessionToken": session.token}

That's it. The app works.

What I'd Do Differently

Start with the library docs, not Apple's official docs. @react-native-apple-authentication README answers 90% of questions. Apple's docs are reference material.

Put team ID in XCode immediately. This is the #1 blocker. Do it first, before anything else.

Validate tokens on the backend. Never trust client-side validation. The extra latency per login is worth the security.

Cache Apple's public keys. The keys endpoint is reliable, but caching with a 24-hour TTL saves repeated network calls.

Test with TestFlight early. The behavior changes between Expo Preview and a real TestFlight build. I didn't notice until App Store submission.

The Lesson

Sign in with Apple looks simple from the outside. "Just press a button" is how it feels to users. Under the hood: team IDs, bundle IDs, redirect URIs, JWT validation, deep linking — all scattered across different Apple documentation pages.

Three days on this probably saved 10 days of debugging production issues and App Store rejections.

Now you can do it in an afternoon.

Enjoyed this post?

Subscribe to get new posts in your inbox every week. No spam, unsubscribe anytime.

Get new posts in your inbox

Weekly insights on AI, growth engineering, and building.