kavachOS
Authentication

Apple

Add Sign in with Apple to your KavachOS application. Covers credentials, client secret generation, iOS apps, and local development.

Sign in with Apple uses OAuth 2.0 with a few Apple-specific requirements: the client secret is a short-lived JWT you generate from a private key, and Apple only returns the user's name on the very first authorization. Plan your data model to store it immediately.

Get credentials from Apple

Register an App ID

  1. Sign in to Apple Developer and go to Certificates, Identifiers & Profiles.
  2. Under Identifiers, click + and choose App IDs.
  3. Select App as the type, then fill in your bundle identifier (e.g. com.example.app).
  4. Scroll to Capabilities and enable Sign In with Apple.
  5. Save the App ID.

Create a Services ID

The Services ID is your OAuth client_id for web and non-iOS flows.

  1. Under Identifiers, click + and choose Services IDs.
  2. Enter a description and an identifier (e.g. com.example.app.auth).
  3. Enable Sign In with Apple.
  4. Click Configure next to Sign In with Apple:
    • Set your Primary App ID to the one you just created.
    • Add your domain (e.g. auth.example.com — no trailing slash, no protocol).
    • Add your Return URL (e.g. https://auth.example.com/auth/oauth/apple/callback).
  5. Save and register.

Create a private key

  1. Under Keys, click +.
  2. Name the key and enable Sign In with Apple.
  3. Click Configure and select your Primary App ID.
  4. Download the .p8 key file — you can only download it once.
  5. Note your Key ID and Team ID (visible at the top right of the developer portal).

Store the .p8 file somewhere safe and never commit it to version control. You will use it to sign a JWT locally, then discard the file from your build environment.

Configuration

lib/kavach.ts
import { createKavach } from 'kavachos';
import { oauth } from 'kavachos/plugins/oauth';

const kavach = await createKavach({
  database: { provider: 'postgres', url: process.env.DATABASE_URL! },
  secret: process.env.KAVACH_SECRET!,
  baseUrl: 'https://auth.example.com',
  plugins: [
    oauth({
      providers: [
        {
          id: 'apple', 
          clientId: process.env.APPLE_CLIENT_ID!,       // Services ID
          clientSecret: process.env.APPLE_CLIENT_SECRET!, // Generated JWT (see below)
        },
      ],
    }),
  ],
});

Generate the client secret

Apple does not accept a static client secret. Instead, you generate a JWT signed with your .p8 private key. The JWT is valid for up to 6 months, so you can generate it once and rotate it before it expires.

scripts/generate-apple-secret.ts
import { SignJWT, importPKCS8 } from 'jose'; 
import { readFileSync } from 'node:fs';

const teamId = process.env.APPLE_TEAM_ID!;       // 10-character string, e.g. "ABC1234567"
const clientId = process.env.APPLE_CLIENT_ID!;   // Services ID, e.g. "com.example.app.auth"
const keyId = process.env.APPLE_KEY_ID!;         // Key ID from the portal
const privateKeyPem = readFileSync('./AuthKey_XXXXXXXXXX.p8', 'utf-8');

const privateKey = await importPKCS8(privateKeyPem, 'ES256'); 

const clientSecret = await new SignJWT({}) 
  .setProtectedHeader({ alg: 'ES256', kid: keyId }) 
  .setIssuer(teamId) 
  .setIssuedAt() 
  .setAudience('https://appleid.apple.com') 
  .setSubject(clientId) 
  .setExpirationTime('180d') 
  .sign(privateKey); 

console.log(clientSecret); // paste into APPLE_CLIENT_SECRET

Run this script locally once, copy the output JWT, and set it as your APPLE_CLIENT_SECRET environment variable. Re-run before the 6-month window closes.

The jose library is already a dependency of kavachos/core, so you do not need to install it separately.

Environment variables

.env
APPLE_CLIENT_ID=com.example.app.auth
APPLE_CLIENT_SECRET=eyJ...  # JWT generated by the script above
APPLE_TEAM_ID=ABC1234567
APPLE_KEY_ID=XXXXXXXXXX

Only APPLE_CLIENT_ID and APPLE_CLIENT_SECRET are needed at runtime. The Team ID and Key ID are only used when regenerating the secret.

iOS native apps

On iOS, use the App ID (bundle identifier, e.g. com.example.app) as clientId — not the Services ID. The Services ID is for web only. Pass appBundleIdentifier in the provider config alongside clientId if your backend handles both flows.

The native Sign In with Apple flow does not go through a browser redirect. Apple's SDK returns an authorization code and a user object directly in the app. Forward both to your server and exchange the code for a session using the same /auth/oauth/apple endpoint.

Localhost and development

Apple requires HTTPS for redirect URIs. localhost will not work. Options:

  • ngrokngrok http 3000 gives you a public HTTPS URL instantly.
  • cloudflared tunnel — persistent tunnel with a stable subdomain.
  • mkcert + local proxy — run a local HTTPS reverse proxy with a self-signed cert trusted by your browser.

Update both your Apple Services ID redirect URI and your baseUrl config to match the tunnel URL while developing.

User data

Apple embeds user claims in the id_token JWT returned from the token endpoint. KavachOS decodes this automatically.

FieldNotes
idStable Apple user identifier — a 24-character opaque string
emailMay be a private relay address (random@privaterelay.appleid.com) if the user chose to hide their email
nameOnly available on the first authorization — store it on your side immediately

Apple returns name only once, on the very first sign-in, via a user form-post field alongside the authorization code. On every subsequent sign-in the field is absent. KavachOS stores it after first auth, but make sure your onUserCreated hook persists it before returning.

Endpoints

MethodPathDescription
POST/auth/oauth/appleInitiate Apple sign-in. Returns a redirect URL to send the user to.
GET/auth/oauth/apple/callbackHandle the callback from Apple after the user authorizes.

Both endpoints are registered automatically when the oauth plugin is configured with id: 'apple'.

On this page