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
- Sign in to Apple Developer and go to Certificates, Identifiers & Profiles.
- Under Identifiers, click + and choose App IDs.
- Select App as the type, then fill in your bundle identifier (e.g.
com.example.app). - Scroll to Capabilities and enable Sign In with Apple.
- Save the App ID.
Create a Services ID
The Services ID is your OAuth client_id for web and non-iOS flows.
- Under Identifiers, click + and choose Services IDs.
- Enter a description and an identifier (e.g.
com.example.app.auth). - Enable Sign In with Apple.
- 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).
- Save and register.
Create a private key
- Under Keys, click +.
- Name the key and enable Sign In with Apple.
- Click Configure and select your Primary App ID.
- Download the
.p8key file — you can only download it once. - 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
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.
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_SECRETRun 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
APPLE_CLIENT_ID=com.example.app.auth
APPLE_CLIENT_SECRET=eyJ... # JWT generated by the script above
APPLE_TEAM_ID=ABC1234567
APPLE_KEY_ID=XXXXXXXXXXOnly 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:
- ngrok —
ngrok http 3000gives 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.
| 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 |
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
| 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'.