apple

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Apple Sign In Emulator

Apple登录模拟器

Sign in with Apple emulation with authorization code flow, PKCE support, RS256 ID tokens, and OIDC discovery.
支持授权码流程、PKCE、RS256 ID令牌和OIDC发现的Apple登录模拟工具。

Start

启动

bash
undefined
bash
undefined

Apple only

Apple only

npx emulate --service apple
npx emulate --service apple

Default port (when run alone)

Default port (when run alone)


Or programmatically:

```typescript
import { createEmulator } from 'emulate'

const apple = await createEmulator({ service: 'apple', port: 4004 })
// apple.url === 'http://localhost:4004'

或者通过编程方式使用:

```typescript
import { createEmulator } from 'emulate'

const apple = await createEmulator({ service: 'apple', port: 4004 })
// apple.url === 'http://localhost:4004'

Pointing Your App at the Emulator

将你的应用指向模拟器

Environment Variable

环境变量

bash
APPLE_EMULATOR_URL=http://localhost:4004
bash
APPLE_EMULATOR_URL=http://localhost:4004

OAuth URL Mapping

OAuth URL映射

Real Apple URLEmulator URL
https://appleid.apple.com/.well-known/openid-configuration
$APPLE_EMULATOR_URL/.well-known/openid-configuration
https://appleid.apple.com/auth/authorize
$APPLE_EMULATOR_URL/auth/authorize
https://appleid.apple.com/auth/token
$APPLE_EMULATOR_URL/auth/token
https://appleid.apple.com/auth/keys
$APPLE_EMULATOR_URL/auth/keys
https://appleid.apple.com/auth/revoke
$APPLE_EMULATOR_URL/auth/revoke
真实Apple URL模拟器URL
https://appleid.apple.com/.well-known/openid-configuration
$APPLE_EMULATOR_URL/.well-known/openid-configuration
https://appleid.apple.com/auth/authorize
$APPLE_EMULATOR_URL/auth/authorize
https://appleid.apple.com/auth/token
$APPLE_EMULATOR_URL/auth/token
https://appleid.apple.com/auth/keys
$APPLE_EMULATOR_URL/auth/keys
https://appleid.apple.com/auth/revoke
$APPLE_EMULATOR_URL/auth/revoke

Auth.js / NextAuth.js

Auth.js / NextAuth.js

typescript
import Apple from '@auth/core/providers/apple'

Apple({
  clientId: process.env.APPLE_CLIENT_ID,
  clientSecret: process.env.APPLE_CLIENT_SECRET,
  authorization: {
    url: `${process.env.APPLE_EMULATOR_URL}/auth/authorize`,
    params: { scope: 'openid email name', response_mode: 'form_post' },
  },
  token: {
    url: `${process.env.APPLE_EMULATOR_URL}/auth/token`,
  },
  jwks_endpoint: `${process.env.APPLE_EMULATOR_URL}/auth/keys`,
})
typescript
import Apple from '@auth/core/providers/apple'

Apple({
  clientId: process.env.APPLE_CLIENT_ID,
  clientSecret: process.env.APPLE_CLIENT_SECRET,
  authorization: {
    url: `${process.env.APPLE_EMULATOR_URL}/auth/authorize`,
    params: { scope: 'openid email name', response_mode: 'form_post' },
  },
  token: {
    url: `${process.env.APPLE_EMULATOR_URL}/auth/token`,
  },
  jwks_endpoint: `${process.env.APPLE_EMULATOR_URL}/auth/keys`,
})

Passport.js

Passport.js

typescript
import { Strategy as AppleStrategy } from 'passport-apple'

const APPLE_URL = process.env.APPLE_EMULATOR_URL ?? 'https://appleid.apple.com'

new AppleStrategy({
  clientID: process.env.APPLE_CLIENT_ID,
  teamID: process.env.APPLE_TEAM_ID,
  keyID: process.env.APPLE_KEY_ID,
  callbackURL: 'http://localhost:3000/api/auth/callback/apple',
  authorizationURL: `${APPLE_URL}/auth/authorize`,
  tokenURL: `${APPLE_URL}/auth/token`,
}, verifyCallback)
typescript
import { Strategy as AppleStrategy } from 'passport-apple'

const APPLE_URL = process.env.APPLE_EMULATOR_URL ?? 'https://appleid.apple.com'

new AppleStrategy({
  clientID: process.env.APPLE_CLIENT_ID,
  teamID: process.env.APPLE_TEAM_ID,
  keyID: process.env.APPLE_KEY_ID,
  callbackURL: 'http://localhost:3000/api/auth/callback/apple',
  authorizationURL: `${APPLE_URL}/auth/authorize`,
  tokenURL: `${APPLE_URL}/auth/token`,
}, verifyCallback)

Seed Config

种子配置

yaml
apple:
  users:
    - email: testuser@icloud.com
      name: Test User
      given_name: Test
      family_name: User
    - email: private@example.com
      name: Private User
      is_private_email: true
  oauth_clients:
    - client_id: com.example.app
      team_id: TEAM001
      name: My Apple App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/apple
When no OAuth clients are configured, the emulator accepts any
client_id
. With clients configured, strict validation is enforced for
client_id
and
redirect_uri
.
Users with
is_private_email: true
get a generated
@privaterelay.appleid.com
email in the
id_token
instead of their real email.
yaml
apple:
  users:
    - email: testuser@icloud.com
      name: Test User
      given_name: Test
      family_name: User
    - email: private@example.com
      name: Private User
      is_private_email: true
  oauth_clients:
    - client_id: com.example.app
      team_id: TEAM001
      name: My Apple App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/apple
当未配置OAuth客户端时,模拟器接受任意
client_id
。配置客户端后,会对
client_id
redirect_uri
进行严格验证。
设置
is_private_email: true
的用户,其
id_token
中会使用生成的
@privaterelay.appleid.com
邮箱,而非真实邮箱。

API Endpoints

API端点

OIDC Discovery

OIDC发现

bash
curl http://localhost:4004/.well-known/openid-configuration
Returns the standard OIDC discovery document with all endpoints pointing to the emulator:
json
{
  "issuer": "http://localhost:4004",
  "authorization_endpoint": "http://localhost:4004/auth/authorize",
  "token_endpoint": "http://localhost:4004/auth/token",
  "jwks_uri": "http://localhost:4004/auth/keys",
  "revocation_endpoint": "http://localhost:4004/auth/revoke",
  "response_types_supported": ["code"],
  "subject_types_supported": ["pairwise"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "email", "name"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "response_modes_supported": ["query", "fragment", "form_post"]
}
bash
curl http://localhost:4004/.well-known/openid-configuration
返回标准OIDC发现文档,所有端点均指向模拟器:
json
{
  "issuer": "http://localhost:4004",
  "authorization_endpoint": "http://localhost:4004/auth/authorize",
  "token_endpoint": "http://localhost:4004/auth/token",
  "jwks_uri": "http://localhost:4004/auth/keys",
  "revocation_endpoint": "http://localhost:4004/auth/revoke",
  "response_types_supported": ["code"],
  "subject_types_supported": ["pairwise"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "email", "name"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "response_modes_supported": ["query", "fragment", "form_post"]
}

JWKS

JWKS

bash
curl http://localhost:4004/auth/keys
Returns an RSA public key (
kid
:
emulate-apple-1
) for verifying
id_token
signatures.
bash
curl http://localhost:4004/auth/keys
返回用于验证
id_token
签名的RSA公钥(
kid
:
emulate-apple-1
)。

Authorization

授权

bash
undefined
bash
undefined

Browser flow: redirects to a user picker page

Browser flow: redirects to a user picker page

curl -v "http://localhost:4004/auth/authorize?\ client_id=com.example.app&
redirect_uri=http://localhost:3000/api/auth/callback/apple&\ scope=openid+email+name&
response_type=code&
state=random-state&
nonce=random-nonce&
response_mode=form_post"

Query parameters:

| Param | Description |
|-------|-------------|
| `client_id` | OAuth client ID (Apple Services ID) |
| `redirect_uri` | Callback URL |
| `scope` | Space-separated scopes (`openid email name`) |
| `state` | Opaque state for CSRF protection |
| `nonce` | Nonce for ID token (optional) |
| `response_mode` | `query` (default), `form_post`, or `fragment` |

The emulator renders an HTML page where you select a seeded user. After selection, it redirects (or auto-submits a form for `form_post`) to `redirect_uri` with `code` and `state`. On the **first** authorization per user/client pair, a `user` JSON blob is also included (matching Apple's real behavior).
curl -v "http://localhost:4004/auth/authorize?\ client_id=com.example.app&
redirect_uri=http://localhost:3000/api/auth/callback/apple&\ scope=openid+email+name&
response_type=code&
state=random-state&
nonce=random-nonce&
response_mode=form_post"

查询参数:

| 参数 | 描述 |
|-------|-------------|
| `client_id` | OAuth客户端ID(Apple服务ID) |
| `redirect_uri` | 回调URL |
| `scope` | 空格分隔的权限范围(`openid email name`) |
| `state` | 用于CSRF保护的随机状态值 |
| `nonce` | ID令牌的随机值(可选) |
| `response_mode` | `query`(默认)、`form_post`或`fragment` |

模拟器会渲染一个HTML页面供你选择预配置的用户。选择后,它会重定向(或针对`form_post`自动提交表单)到`redirect_uri`,并携带`code`和`state`。在**首次**对用户/客户端对进行授权时,还会包含一个`user` JSON对象(与Apple的实际行为一致)。

Token Exchange

令牌交换

bash
curl -X POST http://localhost:4004/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "code=<authorization_code>&\
client_id=com.example.app&\
client_secret=<client_secret>&\
grant_type=authorization_code"
Returns:
json
{
  "access_token": "apple_...",
  "refresh_token": "r_apple_...",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600
}
The
id_token
is an RS256 JWT containing
sub
,
email
,
email_verified
(string),
is_private_email
(string),
real_user_status
,
auth_time
, and optional
nonce
.
bash
curl -X POST http://localhost:4004/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "code=<authorization_code>&\
client_id=com.example.app&\
client_secret=<client_secret>&\
grant_type=authorization_code"
返回结果:
json
{
  "access_token": "apple_...",
  "refresh_token": "r_apple_...",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600
}
id_token
是包含
sub
email
email_verified
(字符串)、
is_private_email
(字符串)、
real_user_status
auth_time
和可选
nonce
的RS256 JWT。

Refresh Token

刷新令牌

bash
curl -X POST http://localhost:4004/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "refresh_token=r_apple_...&\
client_id=com.example.app&\
grant_type=refresh_token"
Returns a new
access_token
and
id_token
. No new
refresh_token
is issued on refresh (matching Apple's behavior).
bash
curl -X POST http://localhost:4004/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "refresh_token=r_apple_...&\
client_id=com.example.app&\
grant_type=refresh_token"
返回新的
access_token
id_token
。刷新时不会颁发新的
refresh_token
(与Apple的实际行为一致)。

Token Revocation

令牌撤销

bash
curl -X POST http://localhost:4004/auth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=apple_..."
Returns
200 OK
. The token is removed from the emulator's token map.
bash
curl -X POST http://localhost:4004/auth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=apple_..."
返回
200 OK
。该令牌会从模拟器的令牌映射中移除。

Common Patterns

常见模式

Full Authorization Code Flow

完整授权码流程

bash
APPLE_URL="http://localhost:4004"
CLIENT_ID="com.example.app"
REDIRECT_URI="http://localhost:3000/api/auth/callback/apple"
bash
APPLE_URL="http://localhost:4004"
CLIENT_ID="com.example.app"
REDIRECT_URI="http://localhost:3000/api/auth/callback/apple"

1. Open in browser (user picks a seeded account)

1. Open in browser (user picks a seeded account)

$APPLE_URL/auth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+name&response_type=code&state=abc&response_mode=form_post

$APPLE_URL/auth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+name&response_type=code&state=abc&response_mode=form_post

2. After user selection, emulator posts to:

2. After user selection, emulator posts to:

$REDIRECT_URI with code=<code>&state=abc (and user JSON on first auth)

$REDIRECT_URI with code=<code>&state=abc (and user JSON on first auth)

3. Exchange code for tokens

3. Exchange code for tokens

curl -X POST $APPLE_URL/auth/token
-H "Content-Type: application/x-www-form-urlencoded"
-d "code=<code>&client_id=$CLIENT_ID&grant_type=authorization_code"
curl -X POST $APPLE_URL/auth/token
-H "Content-Type: application/x-www-form-urlencoded"
-d "code=<code>&client_id=$CLIENT_ID&grant_type=authorization_code"

4. Decode the id_token JWT to get user info

4. Decode the id_token JWT to get user info

undefined
undefined

Private Relay Email

隐私中继邮箱

When a user has
is_private_email: true
in the seed config, the
id_token
will contain a generated
@privaterelay.appleid.com
email instead of the user's real email. This matches Apple's Hide My Email behavior.
当种子配置中的用户设置
is_private_email: true
时,
id_token
将包含生成的
@privaterelay.appleid.com
邮箱,而非用户的真实邮箱。这与Apple的“隐藏我的邮箱”行为一致。