Sign in with Apple
MAIL-181 / MAIL-183 / MAIL-184: Duckbill™ authenticates the application user with Sign in with Apple. This is not Microsoft Graph or Gmail OAuth. See ADR 0028. The signed ia_session cookie from application authentication is unchanged.
Product path
- Open
/loginand choose Sign in with Apple, or start at/api/auth/apple/start. - Apple authenticates the person and
form_postscode,id_token, andstateto the environment callback URL. - Duckbill verifies the Apple
id_token, upsertsinbox_agent.userson Applesub, and sets HTTP-onlyia_session. - The browser continues to an allowlisted path (
/,/connect,/docs, or/accounts) so the user can authorize a mailbox. Provider tokens stay on the server.
Unauthenticated API calls still return UNAUTHENTICATED.
Callback URLs
Register these Return URLs on the Apple Services ID. They must match APPLE_REDIRECT_URI when that variable is set.
| Environment | Return URL |
|---|---|
| Local | http://localhost:3000/api/auth/apple/callback |
| Preview | https://duckbill-dev.jasonkoch.io/api/auth/apple/callback |
| Production | https://duckbill.jasonkoch.io/api/auth/apple/callback |
Also register the hostnames localhost, duckbill-dev.jasonkoch.io, and duckbill.jasonkoch.io as Apple Website URLs / domains.
Apple Developer registration (MAIL-183)
Do this in an Apple Developer Program account. Do not put key material in git, Jira, screenshots, or chat.
- Create or reuse an App ID with the Sign in with Apple capability.
- Create a Services ID (this value is
APPLE_CLIENT_ID). Enable Sign in with Apple and configure the domains and Return URLs above. - Create a Sign in with Apple key. Note
APPLE_KEY_ID. Download the.p8once and store it in the host secret store asAPPLE_PRIVATE_KEY. Never commit the file. - Note the Team ID as
APPLE_TEAM_ID.
Host environment
Names only. Values stay in the Vercel / local secret store.
| Variable | Purpose |
|---|---|
APPLE_CLIENT_ID | Apple Services ID |
APPLE_TEAM_ID | Apple Developer Team ID |
APPLE_KEY_ID | Sign in with Apple key id |
APPLE_PRIVATE_KEY | PKCS#8 contents of the .p8 (PEM or raw body) |
APPLE_REDIRECT_URI | Optional override; defaults from APP_ENV to the table above |
SESSION_SECRET | Existing 32+ character signer for ia_session |
DATABASE_URL | Users and apple_auth_states |
APP_ENV | local, preview, or production (Secure cookies on preview/production) |
loadRequiredEnv does not require the Apple variables. The Apple routes return 503 until they are set so existing hosts keep booting.
Owner session (OWNER_SESSION_ENABLED, OWNER_IDENTITY_SUBJECT) remains an ops escape. Do not put provider tokens in that cookie either.
Token verification
Apple's id_token is RS256. Keys come from https://appleid.apple.com/auth/keys and are RSA JWKs (kty, n, e, kid). The .p8 is only for the ES256 client secret used at /auth/token. Do not import Apple's JWKS as EC (x / y); that fails with key.n undefined.
Session claims
After Apple succeeds, claims are only:
v, userId, iat, exp, csrf
Apple id_token, Apple authorization code, Apple client secret, Microsoft Graph tokens, and Gmail tokens are discarded. They are never session fields.
Migration
0020_apple_identity.sql documents the Apple sub → identity_subject mapping and adds inbox_agent.apple_auth_states for OIDC state / nonce.
