Skip to main content

Apple OAuth

Add Sign in with Apple to your app. By the end of this guide you will have these endpoints working:

EndpointMethodAuthPurpose
/auth/social/apple/redirectGETPublicStart OAuth flow (redirects to Apple)
/auth/social/apple/callbackPOSTPublicApple callback (form_post with authorization code)
/auth/social/exchangePOSTPublicExchange exchangeToken for tokens or challenge
/auth/social/apple/verifyPOSTPublicVerify native iOS identity token

The redirect, callback, and exchange endpoints use the shared social routes. This page adds Apple-specific configuration and the native iOS verify endpoint.

Sample apps

Apple social login is fully implemented in the nauth example apps — see the NestJS, Express, and Fastify examples for social routes, and the React/Angular examples for the login button and callback handling.

Prerequisites​

Step 1: Get Apple Credentials​

Register an App ID (if not already done):

  1. Go to Certificates, Identifiers & Profiles > Identifiers
  2. Create an App ID with the Sign in with Apple capability enabled

Create a Service ID (for web OAuth):

  1. Go to Identifiers > Service IDs and register a new identifier (e.g., com.yourapp.services)
  2. Enable Sign in with Apple and configure:
    • Primary App ID: Select your App ID from above
    • Domains: Your API domain (e.g., api.example.com). Apple rejects localhost and http:// here, so use an HTTPS tunnel or a deployed dev API for local testing
    • Return URLs: https://api.example.com/auth/social/apple/callback

Create a private key:

  1. Go to Keys and create a new key
  2. Enable Sign in with Apple
  3. Download the .p8 file (you can only download it once)
  4. Note the Key ID shown after creation

Note your credentials:

  • Client ID = Service ID identifier (e.g., com.yourapp.services)
  • Team ID = Found in Membership details (e.g., ABC123DEF4)
  • Key ID = Shown after key creation (e.g., XYZ789ABC0)
  • Private Key = Contents of the .p8 file

Step 2: Install​

npm install @nauth-toolkit/social-apple

Step 3: Configure​

config/auth.config.ts
social: {
apple: {
enabled: true,
clientId: 'com.yourapp.services',
teamId: process.env.APPLE_TEAM_ID,
keyId: process.env.APPLE_KEY_ID,
privateKeyPem: process.env.APPLE_PRIVATE_KEY_PEM,
callbackUrl: `${process.env.API_BASE_URL}/auth/social/apple/callback`,
scopes: ['name', 'email'],
autoLink: true,
allowSignup: true,
},
},

If your app has both a website and an iOS app, pass both identifiers:

config/auth.config.ts
social: {
apple: {
clientId: [
'com.yourapp.services', // Service ID (web OAuth)
'com.yourapp', // App Bundle ID (native iOS)
],
// ...rest of config
},
},

The web flow uses the first client ID (Service ID). Native token verification accepts either.

JWT client secret

Apple does not use a static client secret. nauth-toolkit auto-generates an ES256-signed JWT from your teamId, keyId, and privateKeyPem, stores it in the database, and refreshes it when less than 30 days until expiration (180-day lifetime). No manual JWT management needed.

Private key format

The privateKeyPem field accepts both multi-line PEM format and single-line with \n escapes. Both are normalized automatically.

Step 4: Ensure Form Body Parsing​

Apple sends callbacks as HTTP POST with application/x-www-form-urlencoded body (form_post response mode) instead of URL query parameters. Your backend must parse form-encoded bodies:

  • Express: Built-in via express.urlencoded({ extended: true }) middleware
  • Fastify: Requires the @fastify/formbody plugin
  • NestJS: Handled automatically

The shared social routes already include a POST callback handler for this — see the callbackPost route in How Social Login Works.

Step 5: Add the Verify Route (Optional — iOS Only)​

For native iOS apps using Apple's AuthenticationServices framework. Skip this step if you only need web OAuth.

src/auth/social-redirect.controller.ts
import { Inject, BadRequestException, Optional } from '@nestjs/common';
import { VerifyTokenDTO, AuthResponseDTO } from '@nauth-toolkit/nestjs';
import { AppleSocialAuthService } from '@nauth-toolkit/social-apple/nestjs';

// Add to your SocialRedirectController:
constructor(
private readonly socialRedirect: SocialRedirectHandler,
@Optional() @Inject(AppleSocialAuthService)
private readonly appleAuth?: AppleSocialAuthService,
) {}

@Public()
@Post('apple/verify')
async verifyApple(@Body() dto: VerifyTokenDTO): Promise<AuthResponseDTO> {
if (!this.appleAuth) throw new BadRequestException('Apple OAuth is not configured');
return await this.appleAuth.verifyToken(dto);
}

Step 6: Frontend​

Trigger the login​

await client.loginWithSocial('apple', {
returnTo: `${window.location.origin}/auth/callback`,
});

This navigates to GET /auth/social/apple/redirect?returnTo=/auth/callback. The backend redirects to Apple's sign-in page.

Handle the callback​

After the user authenticates with Apple, the backend redirects to your returnTo URL. Your callback page handles the rest.

Web Flow: Request and Response Reference​

1. Start redirect​

GET /auth/social/apple/redirect?returnTo=/auth/callback

Backend responds with 302 Redirect to Apple's sign-in page.

2. Apple callback (form_post)​

Apple sends a POST request to your callback URL:

POST /auth/social/apple/callback
Content-Type: application/x-www-form-urlencoded

code=abc123...&state=csrf-token&user={"name":{"firstName":"John","lastName":"Doe"},"email":"john@example.com"}

The user field is only included on the first sign-in. On subsequent sign-ins, Apple omits it entirely. nauth-toolkit stores the name on first sign-in and uses it for all future sessions.

The backend exchanges the code for an Apple access token + ID token, verifies the ID token against Apple's JWKS keys, creates or links the account, and redirects to your frontend.

3. Exchange (JSON/hybrid mode)​

Request body (SocialExchangeDTO):

{
"exchangeToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Response — tokens + user:

{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"accessTokenExpiresAt": 1700000000,
"refreshTokenExpiresAt": 1700600000,
"user": {
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"isEmailVerified": true
}
}

If MFA is configured, the exchange may return a challenge instead — complete it via /auth/respond-challenge as described in How Social Login Works > Challenges.

Native iOS: Request and Response Reference​

For native iOS apps using AuthenticationServices:

Request body (VerifyTokenDTO):

{
"provider": "apple",
"idToken": "eyJraWQiOiJXNldjT0tCIiwiYWxn..."
}

Response — same structure as web flow (tokens + user, or challenge if MFA is required).

The backend verifies the token's JWT signature against Apple's JWKS keys, validates the issuer (https://appleid.apple.com) and audience, and checks that the email is verified. 5-minute clock tolerance handles device/server time differences.

Apple-Specific Behaviors​

Private Email Relay​

Users can choose Hide My Email during sign-in, providing a relay address (e.g., abc123@privaterelay.appleid.com) instead of their real email. This relay:

  • Forwards to the user's real email
  • Is unique per app
  • Works normally for verification codes, MFA, and all email operations

No Profile Pictures​

Apple does not provide profile picture URLs. The picture field is always null in the user profile.

Re-triggering First Sign-In​

If a user needs to re-provide their name (e.g., after account deletion), they must revoke your app's access in Apple ID Settings > Sign in with Apple before signing in again.

What's Next​