Apple OAuth
WIPSign in with Apple. OIDC-compliant. iOS apps require this if you ship any other social sign-in.
Setup
- Sign in at developer.apple.com with a paid Apple Developer account.
- Create an App ID under Certificates, Identifiers & Profiles.
- Enable Sign in with Apple for the App ID.
- Create a Services ID for your web app. Note the identifier
(this is your
clientId). - Add your Domains & Subdomains and Return URLs:
https://app.example.com/auth/apple/callback. - Create a Sign in with Apple key (a .p8 file). Note the key id and your team id.
import { authApple } from '@gentleduck/auth/providers/oauth/apple'
import { readFileSync } from 'node:fs'
export const auth = new AuthEngine({
// ...
providers: [
authApple({
clientId: 'com.example.app', // Services ID
teamId: process.env.APPLE_TEAM_ID!, // 10-character team id
keyId: process.env.APPLE_KEY_ID!, // 10-character key id
privateKey: readFileSync('./AuthKey_KEYID.p8', 'utf-8'), // .p8 file contents
redirectUri: 'https://app.example.com/auth/apple/callback',
stateSigningSecret: process.env.OAUTH_STATE_SECRET!,
scopes: ['name', 'email'],
onSignIn: async ({ profile }) => {
let identity = await auth.identities.findByProviderSub({
providerId: 'apple',
sub: profile.sub,
})
if (!identity) {
identity = await auth.identities.create({
email: profile.email,
emailVerified: true,
profile: {
displayName: profile.name ?? profile.email.split('@')[0],
},
})
await auth.identities.linkProvider(identity.id, {
providerId: 'apple',
sub: profile.sub,
})
}
return { identityId: identity.id }
},
}),
],
})
How Apple is different
- No userinfo endpoint. All claims arrive in the
id_token(JWT). duck-auth verifies the JWT against Apple's JWKS (https://appleid.apple.com/auth/keys) and reads the claims directly. - Name is only sent on first sign-in. Apple includes
given_name/family_nameonly when the user first authorizes your app. On subsequent sign-ins, those fields are missing. Persist them on first sign-in. - Private relay emails. Users can choose to hide their real email
behind a relay address like
xxxx@privaterelay.appleid.com. Mail sent to it is forwarded; the relay maps to the user's real account. You can never see the real email. form_postcallback. Apple POSTs the callback asapplication/x-www-form-urlencoded, not GET. Your callback route needs to accept POST:
// Express
app.post('/auth/apple/callback', authMountSignIn(auth))
// Next.js
// app/auth/apple/callback/route.ts
export const POST = authNextSignIn(auth)
Required: ASCII Apple JS button
Apple's HIG requires a specific button style. duck-auth doesn't ship UI; use Apple's official JS SDK to render the button, or follow the HIG asset bundle.
What the id_token contains
{
iss: 'https://appleid.apple.com',
aud: 'com.example.app',
exp: 1716800900,
iat: 1716800000,
sub: 'A1B2C3D4-...', // stable user id; opaque
email: 'ada@example.com', // or relay address
email_verified: 'true', // strings, not booleans
is_private_email: 'false', // 'true' if relay
// First sign-in only:
given_name: 'Ada',
family_name: 'Lovelace',
}
Common errors
| Error | duck-auth code | Cause |
|---|---|---|
invalid_client | AUTH/PROVIDER_FAILED | Services ID, team ID, or key ID wrong. |
invalid_grant | AUTH/PROVIDER_FAILED | Code reused or expired. |
unauthorized_client | AUTH/PROVIDER_FAILED | The Services ID isn't enabled for Sign in with Apple. |