@mantlejs/auth-apple
Sign in with Apple strategy for Mantle JS. Implements the authorization code flow with response_mode=form_post — no Passport.js dependency. Registers GET /auth/apple and POST /auth/apple/callback on the HTTP transport, then finds or creates a user record and returns a Mantle JWT pair.
Installation
Section titled “Installation”npm install @mantlejs/auth-appleConcepts
Section titled “Concepts”No static client secret
Section titled “No static client secret”Apple does not issue a client secret. Instead, every token exchange is authenticated with a short-lived ES256 JWT signed from your Sign in with Apple private key (the .p8 file). Arctic signs this JWT per exchange (see ADR-002) — you configure teamId, keyId, and privateKey instead of a clientSecret.
POST callback
Section titled “POST callback”Whenever the name/email scopes are requested, Apple requires response_mode=form_post: the callback arrives as an application/x-www-form-urlencoded POST rather than a GET redirect. CSRF protection is the state round-trip through the OAuthStateStore (Apple does not support PKCE for this flow).
Profile from the id_token
Section titled “Profile from the id_token”Apple has no userinfo endpoint. The profile comes from the id_token returned by the token exchange: sub becomes the user ID, email the email. The id_token is obtained directly from Apple’s token endpoint over TLS, so its payload is decoded without a JWKS round-trip.
First-login name capture
Section titled “First-login name capture”Apple sends the user’s name in a user form field on the first authorization only. The plugin reads it into the profile, and find-or-create persists it when the user record is created — subsequent logins without the field lose nothing.
Find-or-create
Section titled “Find-or-create”On callback the plugin searches the configured user service for a record where appleId (configurable) matches the id_token’s sub claim. If no record is found, it creates one with { appleId, email, name } from the profile. The same user is returned on every subsequent sign-in.
Quick start
Section titled “Quick start”import { mantle } from "@mantlejs/mantle";import { express } from "@mantlejs/express";import { auth } from "@mantlejs/auth";import { appleStrategy } from "@mantlejs/auth-apple";
const app = mantle() .configure(express()) .configure(auth({ secret: process.env.JWT_SECRET! })) .configure( appleStrategy({ clientId: process.env.APPLE_CLIENT_ID!, // Services ID, e.g. "com.example.app.web" teamId: process.env.APPLE_TEAM_ID!, keyId: process.env.APPLE_KEY_ID!, privateKey: process.env.APPLE_PRIVATE_KEY!, // PEM contents of the .p8 key }), );
app.listen(3030);Sign-in flow:
- Redirect the browser to
GET /auth/apple - User authenticates with Apple and consents
- Apple POSTs
code,state(anduseron first login) to/auth/apple/callback - The plugin exchanges the code, decodes the id_token, finds or creates the user, and responds:
{ "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "user": { "id": "1", "appleId": "001234.abc...", "email": "alice@icloud.com", "name": "Alice Doe" }}appleStrategy(config)
Section titled “appleStrategy(config)”function appleStrategy(config: AppleStrategyConfig): MantlePlugin;
type AppleStrategyConfig = { clientId: string; // Services ID (the OAuth client_id) teamId: string; // Apple Developer Team ID keyId: string; // Key ID of the Sign in with Apple private key privateKey: string; // PKCS#8 PEM contents of the .p8 key callbackUrl?: string; // Default: '/auth/apple/callback' scope?: string[]; // Default: ['name', 'email'] entity?: string; // Default: 'users' entityIdField?: string; // Default: 'appleId'};| Field | Type | Default | Description |
|---|---|---|---|
clientId |
string |
— | Services ID from the Apple Developer portal (required) |
teamId |
string |
— | Apple Developer Team ID (required) |
keyId |
string |
— | Key ID of the Sign in with Apple key (required) |
privateKey |
string |
— | PEM contents of the downloaded .p8 private key (required) |
callbackUrl |
string |
'/auth/apple/callback' |
Callback path — must match the Return URL registered with Apple |
scope |
string[] |
['name', 'email'] |
Sign in with Apple scopes |
entity |
string |
'users' |
Mantle service used to find or create users |
entityIdField |
string |
'appleId' |
Field on the user record matched against the id_token’s sub claim |
redirectUrl |
string |
none — returns JSON | Frontend URL to redirect to on completion, with tokens (or an error) in the URL fragment — see @mantlejs/auth-oauth |
Routes registered:
| Method | Path | Description |
|---|---|---|
GET |
/auth/apple |
Redirect to Apple’s sign-in page |
POST |
/auth/apple/callback |
Handle the form_post callback, issue Mantle JWT pair |
Must be configured after an HTTP transport and auth().
import type { AppleStrategyConfig } from "@mantlejs/auth-apple";AppleStrategyConfig extends OAuthPluginConfig from @mantlejs/auth-oauth, replacing clientSecret with teamId/keyId/privateKey.
Apple Developer setup
Section titled “Apple Developer setup”- In developer.apple.com, create an App ID with the Sign in with Apple capability
- Create a Services ID (this is your
clientId) and enable Sign in with Apple for it - Register your domain and Return URL:
https://your-domain.com/auth/apple/callback(Apple requires HTTPS — no localhost) - Create a Sign in with Apple key, download the
.p8file (one-time download), and note its Key ID - Find your Team ID on the membership page
Development
Section titled “Development”npx nx build auth-apple # compilenpx nx test auth-apple # run testsnpx nx lint auth-apple # lintPublishing
Section titled “Publishing”npx nx build auth-applecd packages/auth-applenpm publish --access public