Apple Sign-in for Node.js
`apple-signin-auth` is a Node.js library designed to simplify server-side implementation of Apple's 'Sign in with Apple' authentication flow. It provides a comprehensive API for generating authorization URLs, securely creating and managing client secrets (JSON Web Tokens), and exchanging authorization codes obtained from Apple for user access and refresh tokens. The package is currently at version 2.0.0, which mandates Node.js 18 or newer due to its adoption of native `fetch`. The library demonstrates a moderately active release cadence, with recent updates focusing on modernizing its codebase and enhancing resilience. Its key strengths lie in abstracting the complexities of direct interaction with Apple's REST API, handling JWT client secret generation, supporting `code_verifier` for enhanced security, and providing a clear, developer-friendly interface for the complete Apple Sign-in process on backend applications.
Common errors
-
ReferenceError: fetch is not defined
cause Using `apple-signin-auth` version 2.0.0 or higher with a Node.js runtime older than 18.0.0. Version 2.0.0 replaced `node-fetch` with native Node.js `fetch`.fixUpgrade your Node.js environment to version 18.0.0 or newer. Alternatively, downgrade `apple-signin-auth` to a version below 2.0.0 if a Node.js upgrade is not feasible. -
Error: invalid_grant - The authorization code is invalid or has expired.
cause This error typically indicates that the `authorizationCode` passed to `getAuthorizationToken` is either incorrect, has already been used, or has expired. Authorization codes are single-use and short-lived.fixEnsure you are using a fresh, unused `authorizationCode` obtained directly from Apple's redirect. Double-check that your `redirectUri` and `clientID` used in `getAuthorizationUrl` and `getAuthorizationToken` are identical and match your Apple Developer Console configuration. -
Error: invalid_client - The client ID or client secret is invalid.
cause This usually means there's a mismatch or error in the parameters used to generate the `clientSecret` JWT or the `clientID` itself. Common causes include incorrect `clientID`, `teamID`, `keyIdentifier`, or `privateKey` when calling `getClientSecret`, or an expired `clientSecret`.fixVerify that your `clientID`, `teamID`, `keyIdentifier`, and `privateKey` are all correct and match your Apple Developer Account configuration. Ensure the `expAfter` parameter for `getClientSecret` generates a JWT that is still valid. Double-check your `Service ID` configuration in the Apple Developer Console. -
Error: invalid_id_token_public_key - The public key used to verify the ID token is invalid or missing.
cause This can occur when `verifyIdToken` is called but the `kid` (key ID) in the received `id_token` does not match any available public keys from Apple's JWKS endpoint (`https://appleid.apple.com/auth/keys`). This might happen if Apple has rotated its keys or if there's a caching issue.fixThe library internally handles public key fetching and caching. If this error persists, ensure your server has network access to `https://appleid.apple.com/auth/keys`. It's generally recommended not to cache the `id_token` itself long-term on the client side, but rather use the `refreshToken` to obtain new `id_token`s when needed for verification.
Warnings
- breaking Version 2.0.0 of `apple-signin-auth` introduces a breaking change by requiring Node.js >= 18.0.0. This is due to the replacement of the `node-fetch` package with Node.js's native `fetch` API. Applications running on older Node.js versions will encounter `ReferenceError: fetch is not defined`.
- gotcha Implementing 'Sign in with Apple' requires significant prerequisite setup within your Apple Developer Program account. This includes enrolling in the program, creating specific App IDs and Service IDs, and generating a private key for your Service ID. Incorrect setup will lead to authentication failures.
- gotcha The `state` parameter in the authorization URL options is critical for CSRF (Cross-Site Request Forgery) protection. It must be an unguessable random string generated by your server and verified upon callback. Failure to properly use and validate the `state` parameter can expose your application to CSRF attacks.
- gotcha The `privateKey` used for generating the client secret JWT should be handled with extreme care. Hardcoding it directly in source code or exposing it in client-side bundles is a severe security risk. In production, this key must be loaded from a secure environment variable or a secure file system location.
- gotcha Apple only provides the user's `name` and `email` during the *first* sign-in attempt. Subsequent sign-ins will typically return `null` for these fields, providing only the unique `userIdentifier`. Your application must store this information upon the initial registration.
Install
-
npm install apple-signin-auth -
yarn add apple-signin-auth -
pnpm add apple-signin-auth
Imports
- appleSignin
const appleSignin = require('apple-signin-auth');import appleSignin from 'apple-signin-auth';
- getAuthorizationUrl
import appleSignin from 'apple-signin-auth'; const authorizationUrl = appleSignin.getAuthorizationUrl(...);
import { getAuthorizationUrl } from 'apple-signin-auth'; - getClientSecret
import { getClientSecret } from 'apple-signin-auth'; - getAuthorizationToken
import { getAuthorizationToken } from 'apple-signin-auth';
Quickstart
import { getAuthorizationUrl, getClientSecret, getAuthorizationToken } from 'apple-signin-auth';
import type { AuthorizationTokenResponse } from 'apple-signin-auth';
// --- Configuration (replace with your actual values) ---
const CLIENT_ID = process.env.APPLE_CLIENT_ID ?? 'com.example.app';
const TEAM_ID = process.env.APPLE_TEAM_ID ?? 'YOUR_APPLE_TEAM_ID';
const KEY_IDENTIFIER = process.env.APPLE_KEY_IDENTIFIER ?? 'YOUR_KEY_IDENTIFIER';
const PRIVATE_KEY_STRING = process.env.APPLE_PRIVATE_KEY_STRING ?? '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'; // Make sure this is secure in production
const REDIRECT_URI = process.env.APPLE_REDIRECT_URI ?? 'http://localhost:3000/auth/apple/callback';
async function handleAppleSignIn(authorizationCode: string, state?: string) {
// 1. Generate the client secret required for token exchange
const clientSecret = getClientSecret({
clientID: CLIENT_ID,
teamID: TEAM_ID,
privateKey: PRIVATE_KEY_STRING,
keyIdentifier: KEY_IDENTIFIER,
expAfter: 15777000, // Optional: JWT expiration time (default is 5 minutes)
});
// 2. Exchange the authorization code for access and refresh tokens
try {
const tokenResponse: AuthorizationTokenResponse = await getAuthorizationToken(authorizationCode, {
clientID: CLIENT_ID,
redirectUri: REDIRECT_URI,
clientSecret: clientSecret,
// code_verifier: 'your_code_verifier_if_used' // Optional, for PKCE flow
});
console.log('Successfully exchanged code for tokens:', tokenResponse);
const { access_token, refresh_token, id_token, expires_in } = tokenResponse;
// You would typically decode and verify the ID token here
// and then save user session, profile data, tokens in your database.
// Example: const decodedIdToken = await appleSignin.verifyIdToken(id_token, { audience: CLIENT_ID });
// console.log('Decoded ID Token:', decodedIdToken);
return tokenResponse;
} catch (err) {
console.error('Error during Apple Sign-in token exchange:', err);
throw err;
}
}
// Example usage for generating authorization URL (typically done on frontend or for redirect)
function getSignInUrl() {
const options = {
clientID: CLIENT_ID,
redirectUri: REDIRECT_URI,
state: 'random_csrf_token_here', // IMPORTANT: Generate a secure random string
responseMode: 'form_post', // Or 'query', 'fragment'
scope: 'email name', // Request email and name
};
const authorizationUrl = getAuthorizationUrl(options);
console.log('Apple Sign-in Authorization URL:', authorizationUrl);
return authorizationUrl;
}
// Simulate an incoming authorization code after user redirects
const simulateAuthorizationCode = async () => {
console.log('\n--- Initiating simulated Apple Sign-in flow ---');
getSignInUrl(); // This URL would be visited by the user in a browser
// In a real application, 'some_authorization_code' and 'some_state' would come from the redirect callback
const mockCode = 'MOCK_APPLE_AUTHORIZATION_CODE';
const mockState = 'random_csrf_token_here';
try {
const result = await handleAppleSignIn(mockCode, mockState);
console.log('\nSimulated Sign-in successful:', result);
} catch (error) {
console.error('\nSimulated Sign-in failed:', error);
}
};
simulateAuthorizationCode();