> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kavachos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<Steps>
  <Step>
    ### Register an App ID

    1. Sign in to [Apple Developer](https://developer.apple.com/account) 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.
  </Step>

  <Step>
    ### 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.
  </Step>

  <Step>
    ### 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).
  </Step>
</Steps>

<Warning>
  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.
</Warning>

## Configuration

```typescript title="lib/kavach.ts" theme={"system"}
import { createKavach } from 'kavachos';
import { oauth } from 'kavachos/auth';

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', // [!code highlight]
          clientId: process.env.APPLE_CLIENT_ID!,       // Services ID // [!code highlight]
          clientSecret: process.env.APPLE_CLIENT_SECRET!, // Generated JWT (see below) // [!code highlight]
        },
      ],
    }),
  ],
});
```

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

```typescript title="scripts/generate-apple-secret.ts" theme={"system"}
import { SignJWT, importPKCS8 } from 'jose'; // [!code highlight]
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'); // [!code highlight]

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

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.

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

## Environment variables

```bash title=".env" theme={"system"}
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

<Info>
  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.
</Info>

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:

* **[ngrok](https://ngrok.com/)**, `ngrok http 3000` gives you a public HTTPS URL instantly.
* **[cloudflared tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/local-management/)**, 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.

| Field   | Notes                                                                                                    |
| ------- | -------------------------------------------------------------------------------------------------------- |
| `id`    | Stable Apple user identifier, a 24-character opaque string                                               |
| `email` | May be a private relay address (`random@privaterelay.appleid.com`) if the user chose to hide their email |
| `name`  | Only available on the first authorization, store it on your side immediately                             |

<Warning>
  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.
</Warning>

## Endpoints

| Method | Path                         | Description                                                         |
| ------ | ---------------------------- | ------------------------------------------------------------------- |
| `POST` | `/auth/oauth/apple`          | Initiate Apple sign-in. Returns a redirect URL to send the user to. |
| `GET`  | `/auth/oauth/apple/callback` | Handle the callback from Apple after the user authorizes.           |

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