Skip to content
Development
Skill

/apple

Emulated Sign in with Apple / Apple OIDC for local development and testing. Use when the user needs to test Apple sign-in locally, emulate Apple OIDC discovery, handle Apple token exchange, configure Apple OAuth clients, or work with Apple userinfo without hitting real Apple

From plugin
emulate
1.5k14 skills
Install
$ npx -y skills add vercel-labs/emulate --skill apple --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/apple

Context preview

The summary Claude sees to decide when to auto-load this skill.

Emulated Sign in with Apple / Apple OIDC for local development and testing. Use when the user needs to test Apple sign-in locally, emulate Apple OIDC discovery, handle Apple token exchange, configure Apple OAuth clients, or work with Apple userinfo without hitting real Apple

SKILL.md

apple.SKILL.md
name: apple
description: Emulated Sign in with Apple / Apple OIDC for local development and testing. Use when the user needs to test Apple sign-in locally, emulate Apple OIDC discovery, handle Apple token exchange, configure Apple OAuth clients, or work with Apple userinfo without hitting real Apple APIs. Triggers include "Apple OAuth", "emulate Apple", "mock Apple login", "test Apple sign-in", "Sign in with Apple", "Apple OIDC", "local Apple auth", or any task requiring a local Apple OAuth/OIDC provider.
allowed-tools: Bash(npx emulate:*), Bash(emulate:*), Bash(curl:*)

Apple Sign In Emulator

Sign in with Apple emulation with authorization code flow, PKCE support, RS256 ID tokens, and OIDC discovery.

Start

# Apple only
npx emulate --service apple

# Default port (when run alone)
# http://localhost:4000

Or programmatically:

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

APPLE_EMULATOR_URL=http://localhost:4004

OAuth URL Mapping

| Real Apple URL | Emulator 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)

Seed Config

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.

API Endpoints

OIDC Discovery

curl http://localhost:4004/.well-known/openid-configuration

Returns the standard OIDC discovery document with all endpoints pointing to the emulator:

{
  "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

Returns an RSA public key (`kid`: `emulate-apple-1`) for verifying `id_token` signatures.

Authorization

# 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).

Token Exchange

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:

{
  "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`.

Refresh Token

curl -X POST http://localhost:4004/a
Read more
Ships withemulate

Local drop-in replacement services for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.

Get the whole plugin
Stats
1,543
Stars
109
Forks
Active
Maintenance
TypeScript
Language
Apache-2.0
License
20d ago
Last commit
4mo ago
Created

Repo: vercel-labs/emulate

Other skills on emulate.