关于 apple
apple 是用于测试 Apple 登录集成的身份认证模拟器,面向接入 Sign in with Apple 的应用开发者。它提供授权码流程、PKCE、RS256 ID Token 和 OIDC 发现端点。
主要功能
- 01授权码认证流程
- 02支持 PKCE
- 03签发 RS256 ID Token
- 04提供 OIDC 发现与 JWKS
- 05模拟令牌刷新和撤销
适用场景
- 01测试 Apple 登录回调
- 02验证 PKCE 授权码交换
- 03测试 ID Token 签名校验
- 04检查私密转发邮箱行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill apple安装后在 Claude Code 中输入/apple确认已加载。
详细介绍
Apple 登录模拟器
模拟使用 Apple 登录,支持授权码流程、PKCE、RS256 ID 令牌和 OIDC 发现。
启动
本地监听器默认绑定到 127.0.0.1。如需从容器或其他计算机访问,可向 CLI 传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外公布 URL。
# Apple only
npx emulate --service apple
# Default port (when run alone)
# http://localhost:4000
也可以通过编程方式使用:
import { createEmulator } from 'emulate'
const apple = await createEmulator({ service: 'apple', port: 4004 })
// apple.url === 'http://localhost:4004'
将应用指向模拟器
环境变量
APPLE_EMULATOR_URL=http://localhost:4004
OAuth URL 映射
| 真实 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
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
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)
种子配置
apple:
users:
- email: [email protected]
name: Test User
given_name: Test
family_name: User
- email: [email protected]
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 端点
OIDC 发现
curl http://localhost:4004/.well-known/openid-configuration
返回标准 OIDC 发现文档,其中所有端点都指向模拟器:
{
"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
curl http://localhost:4004/auth/keys
返回一个 RSA 公钥(kid:emulate-apple-1),用于验证 id_token 签名。
授权
# 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"
查询参数:
| 参数 | 描述 |
|---|---|
client_id | OAuth 客户端 ID(Apple Services ID) |
redirect_uri | 回调 URL |
scope | 以空格分隔的作用域(openid email name) |
state | 用于防范 CSRF 的不透明状态值 |
nonce | ID 令牌的 nonce(可选) |
response_mode | query(默认)、form_post 或 fragment |
模拟器会呈现一个 HTML 页面,供你选择已添加种子的用户。选择后,它会将用户重定向到 redirect_uri(若为 form_post,则自动提交表单),并附上 code 和 state。对于每个用户/客户端组合的首次授权,还会包含一个 user JSON 数据块(与 Apple 的实际行为一致)。
令牌交换
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"
返回:
{
"access_token": "apple_...",
"refresh_token": "r_apple_...",
"id_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600
}
id_token 是一个 RS256 JWT,包含 sub、email、email_verified(字符串)、is_private_email(字符串)、real_user_status、auth_time 和可选的 nonce。
刷新令牌
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 的行为一致)。
令牌撤销
curl -X POST http://localhost:4004/auth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=apple_..."
返回 200 OK。该令牌会从模拟器的令牌映射中移除。
常见模式
完整授权码流程
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)
# $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:
# $REDIRECT_URI with code=<code>&state=abc (and user JSON on first auth)
# 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"
# 4. Decode the id_token JWT to get user info
私密转发邮箱
当种子配置中的用户设有 is_private_email: true 时,id_token 会包含生成的 @privaterelay.appleid.com 邮箱,而非用户的真实邮箱。这与 Apple 的“隐藏我的邮箱”行为一致。
与内置模拟器并用自定义模拟器
使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。使用 emulate 中的 defineConfig,将其注册到 emulate.config.ts 中,并与 { emulator: "apple" } 等内置条目并列。运行 npx emulate start --watch 可重新加载导入内容,并在打印出的 /_emulate URL 查看自定义状态。成功重新加载后,运行状态会重置为种子数据。现有的扁平种子配置仍然可用;--config 用于指定文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。