# Apple OAuth Setup

Reference for when the Apple Developer account is ready.
No code changes needed — just fill in the five env vars and restart.

## What you need

- An **Apple Developer Program** membership ($99/yr) — [enroll here](https://developer.apple.com/programs/enroll/)
- The app must be served over **HTTPS** in production (localhost works for dev — see note in Step 2)

## The five env vars

```bash
AUTH_APPLE_CLIENT_ID=        # Services ID identifier  (Step 2)
AUTH_APPLE_CLIENT_SECRET=    # Pre-generated JWT        (Step 4)
AUTH_APPLE_TEAM_ID=          # 10-char team identifier  (Step 1)
AUTH_APPLE_KEY_ID=           # 10-char key identifier   (Step 3)
AUTH_APPLE_PRIVATE_KEY=      # Contents of the .p8 file (Step 3)
```

All five must be non-empty or the Apple button won't appear.
Set any subset to empty to disable the provider without touching code.

---

## Step 1 — Note your Team ID

Sign in to [developer.apple.com](https://developer.apple.com/account).
Your **Team ID** is the 10-character string shown in the top-right corner
next to your name (e.g. `ABCD123456`).

```
AUTH_APPLE_TEAM_ID=ABCD123456
```

---

## Step 2 — Create an App ID

1. [Certificates, IDs & Profiles → Identifiers](https://developer.apple.com/account/resources/identifiers/list)
2. Click **+** → **App IDs** → **App** → Continue
3. **Description**: `Local Cal`
4. **Bundle ID** (Explicit): `com.yourdomain.localcal`
5. Under **Capabilities** scroll to **Sign In with Apple** → check it →
   **Configure** → **Enable as primary App ID** → Done
6. Continue → **Register**

---

## Step 3 — Create a Services ID (the OAuth client ID)

This is what identifies the *web* sign-in flow — separate from the App ID above.

1. **Identifiers → +** → **Services IDs** → Continue
2. **Description**: `Local Cal Web`
3. **Identifier**: `com.yourdomain.localcal.web`
   *(must differ from the Bundle ID)*
4. Continue → **Register**
5. Click the Services ID you just created
6. Enable **Sign In with Apple** → **Configure**
   - **Primary App ID**: select `com.yourdomain.localcal` (Step 2)
   - **Domains and Subdomains**: `yourdomain.com`
     *(no `https://`, no trailing slash, no path)*
   - **Return URLs**:
     ```
     https://yourdomain.com/api/auth/callback/apple
     ```
     > **localhost dev**: add `localhost` as a domain and
     > `http://localhost:3000/api/auth/callback/apple` as a return URL.
     > Apple permits plain HTTP for localhost only.
     > Alternatively use an HTTPS tunnel:
     > [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).
7. Done → Continue → **Save**

```
AUTH_APPLE_CLIENT_ID=com.yourdomain.localcal.web
```

---

## Step 4 — Create a private key

1. [Keys → +](https://developer.apple.com/account/resources/authkeys/list)
2. **Key Name**: `Local Cal Sign In`
3. Check **Sign In with Apple** → **Configure** → select the App ID from Step 2 → Save
4. Continue → **Register**
5. **Download the `.p8` file immediately** — Apple only lets you download it once.
   Store it somewhere safe (password manager, secrets vault).
   The filename will be `AuthKey_XXXXXXXXXX.p8`.

The **Key ID** (`AUTH_APPLE_KEY_ID`) is shown on the key detail page.

```
AUTH_APPLE_KEY_ID=ABC1234567
```

Paste the full contents of the `.p8` file into `AUTH_APPLE_PRIVATE_KEY`:

```bash
# .env.local — wrap multi-line value in double quotes
AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
-----END PRIVATE KEY-----"
```

On deployment platforms (Vercel, Railway, Fly.io) paste the value as-is —
they handle multi-line secrets natively without quotes.

---

## Step 5 — Generate the client secret JWT

Apple does not use a static client secret. Instead you sign a short-lived JWT
with the private key. It is valid for up to **180 days**.

Run this once with Bun:

```ts
// scripts/gen-apple-secret.ts
import { importPKCS8, SignJWT } from "jose";
import { readFileSync } from "fs";

const TEAM_ID   = "ABCD123456";                  // Step 1
const CLIENT_ID = "com.yourdomain.localcal.web"; // Step 3
const KEY_ID    = "ABC1234567";                  // Step 4
const PRIVATE_KEY = readFileSync("./AuthKey_ABC1234567.p8", "utf8");

const key = await importPKCS8(PRIVATE_KEY, "ES256");
const now = Math.floor(Date.now() / 1000);

const jwt = await new SignJWT({})
  .setProtectedHeader({ alg: "ES256", kid: KEY_ID })
  .setIssuer(TEAM_ID)
  .setSubject(CLIENT_ID)
  .setAudience("https://appleid.apple.com")
  .setIssuedAt(now)
  .setExpirationTime(now + 180 * 24 * 60 * 60)
  .sign(key);

console.log(jwt);
```

```bash
bun run scripts/gen-apple-secret.ts
```

Copy the printed JWT:

```
AUTH_APPLE_CLIENT_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6Ii...
```

> **Set a reminder** to regenerate ~5 months from now.
> When the JWT expires the Apple button will silently fail at sign-in.

---

## Completed `.env.local` block

```bash
AUTH_APPLE_CLIENT_ID=com.yourdomain.localcal.web
AUTH_APPLE_CLIENT_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6Ii...
AUTH_APPLE_TEAM_ID=ABCD123456
AUTH_APPLE_KEY_ID=ABC1234567
AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
-----END PRIVATE KEY-----"
```

Restart the dev server — the Apple button appears automatically.

---

## Verifying without a device

```bash
bun test tests/auth.test.ts
```

The tests mock env vars and confirm the Apple provider registers correctly.
A passing suite means the wiring is correct before you touch the portal.

---

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Apple button missing | Any of the 5 vars is empty | All five must be non-empty |
| `invalid_client` | JWT expired or signed with wrong key | Regenerate `AUTH_APPLE_CLIENT_SECRET` (Step 5) |
| `invalid_grant` | `CLIENT_ID` is the Bundle ID, not the Services ID | Use `com.yourdomain.localcal.web` (Step 3) |
| Redirect fails in dev | Localhost not registered as a domain | Add `localhost` + `http://localhost:3000/api/auth/callback/apple` in Step 3 |
| Redirect fails in prod | Domain/return URL not registered | Re-check the Services ID configuration in Step 3 |
| Name is blank after sign-in | Apple only sends the user's name on the **first** sign-in | Expected — store the name on that first callback |
| Email is a relay address | User chose "Hide My Email" | Expected — treat it as canonical, it routes to their real inbox |
