Apple Sign-In

PreviousNext

Step-by-step guide to configuring Apple as an OAuth provider for the Convex Auth starter — the App ID, the Services ID, the signing key, and the JWT client secret you have to generate and rotate.

Before you start

  • A paid Apple Developer account. Sign in with Apple is not available on a free account.
  • A Convex deployment, so you have an HTTP Actions URL to point Apple at.
  • Two identifiers, not one. Apple needs an App ID and a Services ID, and it is the Services ID — not the App ID — that becomes your AUTH_APPLE_ID. Getting these backwards is the most common failure here.

Configure Apple

Create an App ID

  1. Go to Certificates, Identifiers & Profiles.
  2. Select Identifiers in the sidebar, make sure App IDs is selected in the dropdown on the right, and click the + button.
  3. On the Register a New Identifier page, keep App IDs selected and click Continue.
  4. With App selected, click Continue.
  5. Fill in Description, and set Bundle ID to Explicit with your app's identifier — e.g. com.yourcompany.yourapp. It must match the ios.bundleIdentifier in your app.json.
  6. Scroll to Capabilities and check Sign In with Apple.
  7. Click Continue, then Register.

Create a Services ID

  1. Back on Certificates, Identifiers & Profiles, switch the dropdown to Services IDs and click +.
  2. Keep Services IDs selected and click Continue.
  3. Fill in Description and Identifier — e.g. com.yourcompany.yourapp.service. This identifier is what you will set as AUTH_APPLE_ID, so it must be different from the App ID above.
  4. Click Continue, then Register.

Create a signing key

  1. Click Keys in the sidebar, then +.
  2. Give the key a name.
  3. Check Sign In with Apple and click Configure beside it.
  4. Select the App ID you created as the Primary App ID, and click Save.
  5. Click Continue, then Register.
  6. Download the .p8 file.
  7. Note the Key ID shown next to it, and your Team ID from the top right of the developer portal.

Find your Convex HTTP Actions URL

In the Convex dashboard, open Settings → URL & Deploy Key and copy the HTTP Actions URL. It ends in .site, not .cloud — the .cloud URL is what your app talks to for queries and mutations, and it is not what Apple redirects to.

Configure the Services ID for web authentication

  1. Return to Identifiers, switch the dropdown to Services IDs, and click the Services ID you created.

  2. Make sure Sign In with Apple is checked and click Configure.

  3. Set the Primary App ID to your App ID.

  4. In Domains and Subdomains, enter just the domain portion of your HTTP Actions URL — no scheme, no path:

    fast-horse-123.convex.site
    
  5. In Return URLs, enter the full callback URL:

    https://fast-horse-123.convex.site/api/auth/callback/apple
    
  6. Click Next, confirm the values, and click Done.

  7. Back on the Services ID page, click Continue, then Save.

Generate the JWT client secret

Apple does not issue a static secret. You sign one yourself with the .p8 key, using four pieces of information:

ValueWhere it comes from
Team IDTop right of the Apple Developer portal, 10 characters
Services IDThe identifier from step 2, e.g. com.you.app.service
Key IDIn the filename of the key, AuthKey_XXXXXXXXXX.p8
Private keyThe contents of the .p8 file
const jwt = require('jsonwebtoken');
const fs = require('fs');
 
const privateKey = fs.readFileSync('AuthKey_XXXXXXXXXX.p8');
 
const token = jwt.sign(
  {
    iss: 'YOUR_TEAM_ID',
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 15777000, // 6 months
    aud: 'https://appleid.apple.com',
    sub: 'YOUR_SERVICE_ID',
  },
  privateKey,
  { algorithm: 'ES256', header: { kid: 'YOUR_KEY_ID' } }
);
 
console.log(token);

Sign it locally. The .p8 is a private key: pasting it into an online JWT generator hands whoever runs that page the ability to authenticate as your app.

Set the environment variables

pnpm dlx convex env set AUTH_APPLE_ID your_service_id
npx convex env set AUTH_APPLE_SECRET your_generated_jwt

Or add them in the Convex dashboard under Settings → Environment Variables. Set them on your production deployment too, with --prod.

Test it

pnpm dlx expo start

Tap Login with Apple. The browser sheet opens; sign in with an Apple ID.

What Apple sends back

Less than Google, and only once. convex/auth.ts's profile callback reads whatever is present on that first authorization:

convex/auth.ts
Apple({
  profile: (appleInfo) => {
    const name = appleInfo.user
      ? `${appleInfo.user.name.firstName} ${appleInfo.user.name.lastName}`
      : undefined;
 
    return {
      id: appleInfo.sub,
      name: name,
      email: appleInfo.email,
    };
  },
}),

appleInfo.user — the name — is only present on the very first sign-in. Every subsequent one omits it, so name in the returned profile is undefined from then on. If the user picked Hide My Email, email is a private relay address, not their real one. Capture whatever you need at first sign-in; do not build anything that depends on re-reading it later.

When it does not work

What you seeUsually
invalid_clientAUTH_APPLE_ID is the App ID instead of the Services ID
invalid_client after months of workingThe six-month JWT secret expired
Redirect rejectedReturn URL in the Services ID doesn't match /api/auth/callback/apple
Browser closes, nothing happensEXPO_URL/SITE_URL don't cover the redirect — see the redirect allow-list
Name is missing after the first sign-inWorking as intended — Apple sends it once

Next