@arendajaelu/nestjs-passport-apple

中文文档
2.1.0发布于 2026/07/22MITTypeScriptCommonJS近 30 天下载 3.7万

Apple strategy for Passport in Nestjs

原始使用文档
文档目录26 节
  1. Features
  2. Requirements
  3. Installation
  4. Usage
  5. Strategy Setup
  6. Controller Endpoints
  7. Strategy Options
  8. Validate Callback
  9. Profile Object
  10. Private Relay Email Addresses
  11. Beyond Login
  12. Verifying a Native App's Identity Token
  13. Validating a Refresh Token and Obtaining New Tokens
  14. Revoking Tokens (Account Deletion)
  15. Processing Server-to-Server Notifications
  16. TypeScript: Request Typing
  17. Testing
  18. Tutorials
  19. FAQ
  20. How do I implement Apple login in NestJS?
  21. How do I verify an Apple identity token from an iOS app on …
  22. How do I revoke Sign in with Apple tokens for account delet…
  23. Why is the user's name missing after the first Apple login?
  24. Does this work with the new private.icloud.com Hide My Emai…
  25. Can my server get notified when a user revokes access or de…
  26. License

NestJS Passport Apple Strategy — Sign in with Apple for NestJS

npm version npm downloads license node CI

Apple login for NestJS in minutes. A Passport strategy that integrates Sign in with Apple into NestJS applications — Apple OAuth 2.0 authentication, identity token (JWT) verification, refresh token validation, token revocation for account deletion, and Apple's server-to-server notifications, all in one typed, tested package with 900k+ downloads.

Works with @nestjs/passport and AuthGuard, supports web apps (Services ID) and native iOS/macOS apps (App ID), and verifies every token against Apple's public keys before your code ever sees it.

Features

  • 🍎 Plug-and-play Apple login — works with NestJS 10 & 11 via AuthGuard
  • 🔐 Verified before you see it — ID tokens are checked against Apple's public keys; your validate only receives trusted data
  • 🧾 Typed Profile — email, verification status, and Hide My Email flag included
  • 📱 Native app support — verify identity tokens from iOS/macOS apps, with nonce protection (verifyIdToken)
  • ♻️ Session validation — check that a user's Apple authorization is still alive (refreshAccessToken)
  • 🗑️ Account deletion ready — revoke tokens per App Store Guideline 5.1.1(v) (revokeToken)
  • 📨 Server-to-server notifications — verify Apple's signed account events (verifyServerNotification)
  • 🛡️ CSRF protection — optional OAuth state generation and validation
  • ✅ Fully tested — unit and integration suites run the complete flow against a mock Apple server, and published types are compiled by a real strict-mode NestJS consumer in CI

Requirements

  • Node.js 18 or later when used with NestJS 10
  • Node.js 20 or later when used with NestJS 11 (NestJS 11 itself requires Node.js 20)

Installation

npm install @arendajaelu/nestjs-passport-apple

Usage

Strategy Setup

The following example shows all commonly used options:

import { Injectable } from '@nestjs/common';
import { AuthGuard, PassportStrategy } from '@nestjs/passport';
import { Strategy, Profile } from '@arendajaelu/nestjs-passport-apple';

const APPLE_STRATEGY_NAME = 'apple';

@Injectable()
export class AppleStrategy extends PassportStrategy(Strategy, APPLE_STRATEGY_NAME) {
  constructor() {
    super({
      clientID: process.env.APPLE_OAUTH_CLIENT_ID,
      teamID: process.env.APPLE_TEAMID,
      keyID: process.env.APPLE_KEYID,
      key: process.env.APPLE_KEY_CONTENTS,
      // OR
      keyFilePath: process.env.APPLE_KEYFILE_PATH,
      callbackURL: process.env.APPLE_OAUTH_CALLBACK_URL,
      scope: ['email', 'name'],
      // state: true, // recommended in production for CSRF protection; requires express-session
      passReqToCallback: false,
    });
  }

  async validate(_accessToken: string, _refreshToken: string, profile: Profile) {
    return {
      emailAddress: profile.email,
      firstName: profile.name?.firstName || '',
      lastName: profile.name?.lastName || '',
    };
  }
}

@Injectable()
export class AppleOAuthGuard extends AuthGuard(APPLE_STRATEGY_NAME) {}

Note: Register AppleStrategy in the providers array of your module.

Controller Endpoints

import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AppleOAuthGuard } from './strategies/apple.strategy';

@ApiTags('oauth')
@Controller('oauth')
export class OAuthController {
  @Get('apple')
  @UseGuards(AppleOAuthGuard)
  async appleLogin() {}

  @Post('apple/callback')
  @UseGuards(AppleOAuthGuard)
  async appleCallback(@Req() req) {
    return req.user;
  }
}

Apple delivers the authorization callback as an application/x-www-form-urlencoded HTTP POST whenever scopes such as name or email are requested (the strategy always sends response_mode=form_post). The callback route must therefore be a @Post handler with URL-encoded body parsing enabled — NestJS enables this by default. The callback URL must exactly match an HTTPS return URL registered in the Apple Developer portal; Apple does not accept localhost or IP-address callback URLs. Authorization codes are single-use and expire after five minutes.

Strategy Options

OptionRequiredDescription
clientIDYesApple Services ID (or App ID) used as the OAuth client identifier
teamIDYesApple Developer Team ID
keyIDYesIdentifier of the Sign in with Apple private key
keyYes*Contents of the private key. Provide either key or keyFilePath
keyFilePathYes*Path to the private key file; loaded with fs.readFileSync
callbackURLNoOAuth redirect URI; must match a return URL registered with Apple
scopeNoArray of scopes, e.g. ['email', 'name']
stateNoEnables OAuth state generation and validation to protect against CSRF: a random value is stored in the session before redirecting to Apple and compared with the value Apple posts back. Strongly recommended in production. Requires express-session or compatible session middleware. Defaults to false
storeNoCustom state store implementing store(req, callback) and verify(req, state, callback), for deployments without session middleware
sessionKeyNoSession key under which state is stored; defaults to apple:<hostname>
passReqToCallbackNoPasses the request as the first argument of validate; defaults to false
clientSecretExpiryNoLifetime of the generated client secret JWT; defaults to '5 minutes'. Secrets are cached and regenerated automatically before expiry
authorizationURLNoDefaults to https://appleid.apple.com/auth/authorize
tokenURLNoDefaults to https://appleid.apple.com/auth/token
revokeURLNoDefaults to https://appleid.apple.com/auth/revoke
jwksURLNoJWKS endpoint used to fetch Apple's public keys; defaults to https://appleid.apple.com/auth/keys

Validate Callback

After successful authentication, validate receives the accessToken, refreshToken, and verified profile. The value it returns becomes req.user. Store the refreshToken if you intend to validate sessions or revoke access later.

Profile Object

{
  id: string;               // stable Apple user identifier (the `sub` claim)
  provider: 'apple';
  email?: string;
  emailVerified?: boolean;
  isPrivateEmail?: boolean; // true when the address is a Hide My Email relay
  name?: {                  // present only on the user's first authorization
    firstName: string;
    lastName: string;
  };
}

Important: Always associate application accounts with profile.id, not the email address. Apple posts the user object (containing name) only during the user's first authorization — persist it at that point. Because the name originates from the browser form post rather than from the signed identity token, validate and sanitize it before storing or displaying it. Subsequent authorizations do not include the user object; the identity token normally carries the email claim, but applications should tolerate a missing email as well (some managed Apple Accounts have none).

Private Relay Email Addresses

Do not identify or filter Apple relay addresses by inspecting the email domain; rely on the verified isPrivateEmail flag instead. If your email validation or delivery infrastructure filters by domain, it must accept all of Apple's relay domains: private.icloud.com (used for addresses generated from mid-2026 onward) as well as the legacy privaterelay.appleid.com and icloud.com.

Beyond Login

The strategy instance exposes the remaining Sign in with Apple lifecycle operations. Because the strategy is a NestJS provider, it can be injected wherever these operations are needed.

Verifying a Native App's Identity Token

When your backend also serves an iOS/macOS application using ASAuthorizationAppleIDCredential, the app submits an identity token directly. Verify it against Apple's public keys:

const claims = await appleStrategy.verifyIdToken(identityToken, {
  audience: 'com.example.bundleid', // the app's bundle ID, from server-side configuration
  nonce: expectedNonce,             // the nonce your server issued for this sign-in attempt
});
// claims.sub is the stable Apple user identifier
  • audience defaults to the configured clientID; override it when the token was issued for a different client, such as the app's bundle ID.
  • When a nonce is supplied, verification fails unless the token's nonce claim matches exactly. Apple requires nonce validation whenever a nonce is used, as protection against replay attacks.

Security: The expected audience and nonce must come from trusted server-side configuration or server-side session state. Never accept either value from the untrusted request being authenticated.

Validating a Refresh Token and Obtaining New Tokens

const tokens = await appleStrategy.refreshAccessToken(refreshToken);
// tokens: { access_token, token_type, expires_in, id_token }

Apple recommends validating a stored refresh token no more than once per day; more frequent validation risks throttling. An invalid_grant error means the authorization code or refresh token is invalid, expired, already consumed, revoked, or does not match the configured client — in all cases, the application should require the user to authenticate again.

Revoking Tokens (Account Deletion)

App Store Review Guideline 5.1.1(v) requires applications that offer account creation to also offer account deletion, including revocation of Sign in with Apple tokens:

await appleStrategy.revokeToken(refreshToken); // token_type_hint defaults to 'refresh_token'
// or
await appleStrategy.revokeToken(accessToken, 'access_token');

A successful call revokes the token and its associated Sign in with Apple authorization, or confirms that the token was already invalid — Apple returns HTTP 200 in both cases.

Note: Revoking the Apple token does not delete the application account or terminate the application's own sessions. After revocation, the application remains responsible for deleting applicable user data, removing stored tokens, clearing local sessions and cookies, and returning the client to an unauthenticated state.

Processing Server-to-Server Notifications

Apple can notify your server when users change their relationship with your application. Register a notification endpoint URL in the Apple Developer portal; Apple then delivers signed events as an HTTP POST with a JSON body of the form { "payload": "<signed JWS>" }. Verify the signature before acting on any event:

@Post('apple/notifications')
async appleNotifications(@Body() body: { payload: string }) {
  const notification = await this.appleStrategy.verifyServerNotification(body.payload);

  switch (notification.events.type) {
    case 'email-enabled':   // user enabled email forwarding
    case 'email-disabled':  // user disabled email forwarding
      break;
    case 'consent-revoked': // user revoked authorization — end their sessions
      break;
    case 'account-deleted': // user deleted their Apple Account — remove associated data
      break;
  }
}

verifyServerNotification validates the JWS signature against Apple's public keys, along with the issuer and audience, and returns the claims with the events payload parsed. The library performs verification only; deciding how to act on each event remains the application's responsibility. Verification failures throw — treat such requests as forged and reject them.

Replay protection: a verified notification carries a jti (unique notification ID). Because a captured delivery could be replayed to your endpoint while still within its validity window, persist processed jti values (database, Redis, etc.) and reject duplicates before acting on an event:

const accepted = await this.notificationStore.claim(notification.jti);
if (!accepted) {
  throw new ConflictException('Notification already processed');
}

TypeScript: Request Typing

The strategy's type declarations are fully self-contained — no @types/express or @types/passport-strategy required. The request type is generic, so passReqToCallback and custom state stores work with any HTTP framework's request type:

import type { Request } from 'express'; // or FastifyRequest from 'fastify'

export class AppleStrategy extends PassportStrategy(Strategy<Request>, 'apple') {
  constructor() {
    super({ /* options */, passReqToCallback: true });
  }

  validate(req: Request, accessToken: string, refreshToken: string, profile: Profile) {
    return { id: profile.id };
  }
}

Testing

The repository ships with a comprehensive automated test suite built on the Node.js native test runner — no additional test dependencies are required:

npm test

The suite covers two levels:

  • Unit tests (test/strategy.test.js): constructor validation, authorization redirect parameters, Apple error handling, profile claim parsing (including the string and boolean forms of email_verified and is_private_email), revocation and refresh request formats, client secret generation, expiry-driven regeneration, per-client secret caching, and state store behavior (session-based validation, mismatch rejection, and custom stores).
  • Integration tests (test/integration.test.js): the complete authentication flow executed against a local mock of Apple's token and JWKS endpoints with genuine RS256-signed identity tokens, along with negative cases — wrong audience, unknown signing key, expired tokens, nonce mismatches, and forged server notifications.

Tutorials

For a complete, up-to-date walkthrough covering authentication, token revocation, and the full Sign in with Apple lifecycle:

Apple Login with NestJS in 2026–2027: A Complete Guide to Authentication, Token Revocation and More

The original walkthrough of the Apple Developer portal setup is also available:

How to Implement Apple Login with NestJS in Seconds

FAQ

How do I implement Apple login in NestJS?

Install this package, register the strategy shown in Strategy Setup as a NestJS provider, and guard your routes with AuthGuard('apple'). The complete guide walks through every step, including the Apple Developer portal configuration.

How do I verify an Apple identity token from an iOS app on my NestJS backend?

Use verifyIdToken with your app's bundle ID as the audience. It validates the RS256 signature against Apple's JWKS, plus issuer, expiry, and (optionally) nonce.

How do I revoke Sign in with Apple tokens for account deletion?

Call revokeToken with the user's stored refresh token. This satisfies App Store Review Guideline 5.1.1(v), which requires apps offering account creation to also revoke Apple tokens on account deletion.

Why is the user's name missing after the first Apple login?

Apple sends the name (and the user object) only on the user's first authorization. Persist it on first login — see Profile Object.

Does this work with the new private.icloud.com Hide My Email addresses?

Yes. The strategy exposes a verified isPrivateEmail flag, so you never need to match relay domains yourself — see Private Relay Email Addresses.

Can my server get notified when a user revokes access or deletes their Apple Account?

Yes — register a notification endpoint with Apple and verify each event with verifyServerNotification.

License

Licensed under MIT.


Keywords: Sign in with Apple, Apple login NestJS, NestJS Apple OAuth, passport-apple, Apple OAuth2 strategy, apple-signin, Apple ID authentication, verify Apple identity token, Apple token revocation, Apple server-to-server notifications, Hide My Email, NestJS authentication, Passport.js Apple strategy

版本与发布记录

@arendajaelu/nestjs-passport-apple 共发布 12 个版本,首次发布于 2022/08/12;过去 12 个月发布了 1 个版本。

版本发布日期距今
2.1.0latest2026/07/223 个月前
2.0.52025/02/071.7 年前
2.0.42025/02/061.7 年前
2.0.32024/07/152 年前
2.0.22023/12/032 年前
2.0.12023/09/273 年前

关于 @arendajaelu/nestjs-passport-apple 的常见问题

@arendajaelu/nestjs-passport-apple 是什么?

Apple strategy for Passport in Nestjs

如何安装 @arendajaelu/nestjs-passport-apple?

在项目目录运行 npm install @arendajaelu/nestjs-passport-apple,或使用 pnpm add / yarn add / bun add。

@arendajaelu/nestjs-passport-apple 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@arendajaelu/nestjs-passport-apple 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@arendajaelu/nestjs-passport-apple 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@arendajaelu/nestjs-passport-apple 可以商用吗?

@arendajaelu/nestjs-passport-apple 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@arendajaelu/nestjs-passport-apple 的下载量有多少?

根据 npm 官方下载统计,@arendajaelu/nestjs-passport-apple 在 2026/10/07 – 2026/11/05 这 30 天内被下载 37,345 次,日均约 1,245 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@arendajaelu/nestjs-passport-apple 还在维护吗?

最新版本 2.1.0 发布于 2026/07/22(3 个月前);至今共发布 12 个版本,其中过去 12 个月发布了 1 个;首次发布于 2022/08/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。