← All @molecule/* packages · App templates
@molecule/api-oauth-appleProvider bond · auth · API (Node) · v1.0.1 · Apache-2.0
Sign in with Apple OAuth provider for molecule.dev.
npm install @molecule/api-oauth-applenpm · Source on GitHub · Implements @molecule/api-oauth
@molecule/api-oauth-apple is a provider bond on the API (Node) side: it implements the auth core interface (@molecule/api-oauth) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-bond, @molecule/api-http, @molecule/api-oauth, @molecule/api-secrets
Secrets: OAUTH_APPLE_CLIENT_ID, OAUTH_APPLE_TEAM_ID, OAUTH_APPLE_KEY_ID, OAUTH_APPLE_PRIVATE_KEY
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Sign in with Apple OAuth provider for molecule.dev.
Enroll in the Apple Developer Program.
In the Certificates, Identifiers & Profiles console, create:
OAUTH_APPLE_CLIENT_ID.
Configure its Sign in with Apple settings with your domain and the
exact redirect URI(s) you will use (e.g. https://yourapp.com/auth/apple/callback)..p8 file once (Apple does not allow re-download). The 10-character
Key ID becomes OAUTH_APPLE_KEY_ID; the file's contents become
OAUTH_APPLE_PRIVATE_KEY.Locate your Team ID at the top right of the developer console;
set it to OAUTH_APPLE_TEAM_ID.
Configure your API environment:
OAUTH_APPLE_CLIENT_ID — the Services IDOAUTH_APPLE_TEAM_ID — your Apple Developer Team IDOAUTH_APPLE_KEY_ID — the 10-character Key IDOAUTH_APPLE_PRIVATE_KEY — the PKCS8 PEM contents of the .p8 file
(newlines may be encoded as \n)Restart your API so it picks up the environment variables.
Your users should now be able to log in via Apple!
provider
npm install @molecule/api-oauth-apple @molecule/api-bond @molecule/api-http @molecule/api-oauth @molecule/api-secrets jsonwebtoken
npm install -D @types/jsonwebtoken
AppleAuthorizationUrlOptionsOptions for {@link getAuthorizationUrl}.
interface AppleAuthorizationUrlOptions {
/** Opaque state token echoed back in the redirect (CSRF defence). */
state: string
/** Redirect URI — must exactly match a value registered in the Apple developer portal. */
redirectUri: string
/** Space-separated list of scopes. Defaults to `'name email'`. */
scope?: string
/** Optional nonce for replay protection. */
nonce?: string
/** Response mode (defaults to `form_post`, required when `scope` includes `name` or `email`). */
responseMode?: 'query' | 'fragment' | 'form_post'
}
AppleClientSecretInputInputs required to mint an Apple client-secret JWT.
interface AppleClientSecretInput {
/** The Apple Developer Team ID (`iss` claim). */
teamId: string
/** The Apple Services ID / client ID (`sub` claim). */
clientId: string
/** The 10-character Apple Key ID (`kid` JWT header). */
keyId: string
/** PKCS8 PEM-encoded private key contents (the `.p8` file). */
privateKey: string
/** Optional override for token lifetime, in seconds. */
lifetimeSeconds?: number
}
AppleIdTokenClaimsThe decoded payload of an Apple ID token (a JWT issued by Apple at
https://appleid.apple.com). Apple does not expose a userinfo endpoint —
the ID token is the user info.
interface AppleIdTokenClaims {
/** Issuer — always `https://appleid.apple.com`. */
iss: string
/** Audience — the Apple Services ID (`OAUTH_APPLE_CLIENT_ID`). */
aud: string
/** Stable Apple-provided user identifier. */
sub: string
/** Issued-at timestamp (seconds since epoch). */
iat: number
/** Expiration timestamp (seconds since epoch). */
exp: number
/** Optional nonce echoed back from the authorization request. */
nonce?: string
/** Whether `nonce` was supplied. */
nonce_supported?: boolean
/** The user's email address, when permission was granted. */
email?: string
/** Whether Apple verified the email. Apple returns `'true'`/`'false'` strings or booleans. */
email_verified?: boolean | string
/** Whether the email is a private relay address Apple created for the user. */
is_private_email?: boolean | string
/** Real-user-status indicator (0=unsupported, 1=unknown, 2=likely real). */
real_user_status?: number
/** Indexable additional claims so callers may surface vendor extensions. */
[key: string]: unknown
}
AppleJwkA single public-key entry as returned by Apple's JWKS endpoint.
interface AppleJwk {
kty: string
kid: string
use?: string
alg?: string
n: string
e: string
}
AppleTokenResponseTokens returned by Apple's /auth/token endpoint.
interface AppleTokenResponse {
access_token: string
expires_in: number
id_token: string
refresh_token?: string
token_type: string
}
OAuthAuthorizeUrlParamsParameters for building a provider authorization (initiation) URL — the URL the user's browser is redirected to so the provider can authenticate them and send back an authorization code.
interface OAuthAuthorizeUrlParams {
/**
* Absolute URI the provider should redirect the user back to after
* authorization (the app origin, optionally with a path). When omitted,
* the builder leaves `redirect_uri` off the URL so the provider falls
* back to its registered callback URL.
*/
redirectUri?: string
/**
* The CSRF `state` parameter bound to the initiating session (stored in
* an httpOnly cookie by the initiation endpoint and validated by the
* login handler on callback).
*/
state: string
/**
* PKCE code challenge derived (S256) from the per-session code verifier.
* Omit only for providers that do not support PKCE.
*/
codeChallenge?: string
/**
* PKCE challenge method. Always prefer `'S256'`; `'plain'` exists only
* for providers that cannot hash.
*/
codeChallengeMethod?: 'S256' | 'plain'
}
OAuthUserPropsThe properties returned when verifying an OAuth code.
interface OAuthUserProps {
/**
* An alphanumeric username derived from the OAuth provider.
*
* Format: `{provider_username}@{provider_name}`
*/
username: string
/**
* The user's display name from the OAuth provider.
*/
name?: string
/**
* The user's short biography / description from the OAuth provider
* (e.g. GitHub's `bio`, GitLab's `bio`, X's `description`). Omitted when
* the provider exposes no such field.
*/
bio?: string
/**
* URL of the user's profile image from the OAuth provider (e.g. Google's
* `picture`, GitHub/GitLab's `avatar_url`, X's `profile_image_url`).
* Omitted when the provider exposes none (Apple never does; Microsoft
* Graph only serves photos as a binary endpoint behind an extra scope).
*/
avatar?: string
/**
* The user's email address from the OAuth provider.
*/
email?: string
/**
* Whether the OAuth provider has affirmatively verified that the user
* controls this `email` mailbox.
*
* `true` MUST mean the provider proved mailbox ownership (e.g. Google's
* `email_verified`, Apple's `email_verified` ID-token claim). When the
* provider exposes no trustworthy verification signal in the profile data
* the verifier fetched, this MUST be `false`/`undefined` (never optimistically
* `true`) — consumers treat only an explicit `true` as verified.
*
* Consumers (e.g. the user resource's `logInOAuth` handler) use this to
* decide whether a provider-supplied email may be trusted over an existing,
* unverified local account — preventing an unverified squatter from blocking
* the verified mailbox owner.
*/
emailVerified?: boolean
/**
* The OAuth server identifier (e.g., 'github', 'google', 'twitter').
*/
oauthServer: string
/**
* Unique identifier for the user from the OAuth provider.
*/
oauthId: string
/**
* Raw user data from the OAuth provider.
*/
oauthData: Record<string, unknown>
}
OAuthAuthorizeUrlBuilderBuilds the provider's authorization URL for OAuth initiation
(GET /users/oauth/:provider → 302 to this URL). Implementations embed
their own client id, scopes, and authorize endpoint so no consumer ever
hardcodes provider knowledge.
type OAuthAuthorizeUrlBuilder = (params: OAuthAuthorizeUrlParams) => string | null
OAuthVerifierExchanges an OAuth authorization code for user profile information.
Implementations call the provider's token and user-info endpoints,
then return normalized OAuthUserProps for account creation or login.
Returning null means the provider AFFIRMATIVELY rejected the code
(e.g. GitHub's bad_verification_code, an expired/forged code) — the
consumer (logInOAuth) surfaces that as a clean 403 "verification
failed". A thrown error means an infrastructure failure (network,
provider outage) and surfaces as a 500. Implementations MUST NOT throw
for a rejected code — that would misreport a client mistake (or an
attack) as a server fault.
type OAuthVerifier = (
code: string,
codeVerifier?: string,
redirectUri?: string,
) => Promise<OAuthUserProps | null>
createAppleClientSecret(input)Signs an Apple client-secret JWT (ES256) suitable for use as the
client_secret parameter in /auth/token requests.
Note: this function intentionally does not include the private key in any thrown error.
function createAppleClientSecret({
teamId,
clientId,
keyId,
privateKey,
lifetimeSeconds = APPLE_CLIENT_SECRET_DEFAULT_LIFETIME_SECONDS,
}: AppleClientSecretInput): string
input — Team ID, client ID, key ID, and private key.Returns: The compact-serialized JWT.
exchangeCodeForTokens(code, redirectUri)Exchanges an authorization code for Apple-issued tokens.
function exchangeCodeForTokens(code: string, redirectUri?: string): Promise<AppleTokenResponse>
code — The authorization code from the OAuth callback.redirectUri — The redirect URI used in the authorization request. Defaults to APP_ORIGIN.Returns: The token-exchange response, including id_token and (typically) a refresh_token.
getAppleJwks()Fetch Apple's JWKS, returning a kid → JWK map. Cached for {@link JWKS_CACHE_TTL_MS}.
function getAppleJwks(): Promise<Map<string, AppleJwk>>
Returns: A map of kid to JWK suitable for verifying Apple ID tokens.
getAuthorizationUrl(options)Returns the URL to which the user agent should be redirected to start the Sign-in-with-Apple flow.
response_mode=form_post is the default because Apple only returns the
name and email scopes via form-post callbacks.
function getAuthorizationUrl({
state,
redirectUri,
scope = DEFAULT_APPLE_SCOPE,
nonce,
responseMode,
}: AppleAuthorizationUrlOptions): string
options — State, redirect URI, and optional scope/nonce/response mode.Returns: The fully-constructed authorization URL.
getAuthorizeUrl(params)Builds the Sign-in-with-Apple authorization URL for OAuth initiation
(GET /users/oauth/:provider 302s the browser here). Embeds this bond's
client id and default scopes (name email) plus the caller's CSRF
state, so no consumer hardcodes Apple knowledge.
redirect_uri resolves from params.redirectUri falling back to
APP_ORIGIN (the same fallback the token exchange uses) — Apple requires
an exact registered redirect_uri and has no registered-fallback
behavior, so the param is set whenever one resolves.
response_mode=form_post is required by Apple for the name/email
scopes: the callback arrives as an HTTP POST with code/state in the
form body, so redirect_uri must point at a server-side receiver that
forwards them into POST /users/log-in/oauth (see the module JSDoc).
Apple does not support PKCE: the codeChallenge/codeChallengeMethod
params are accepted (the shared initiation handler always sends them) but
deliberately not emitted — form_post keeping the code out of the URL is
the mitigation Apple recommends.
function getAuthorizeUrl({
redirectUri,
state,
// Apple's authorize endpoint does not support PKCE — accept the
// code_challenge/code_challenge_method params from the shared contract
// (the initiation handler always sends them) but deliberately do NOT
// emit them. `response_mode=form_post` (code delivered in a POST body,
// never in the URL) is the mitigation Apple recommends instead.
codeChallenge: _codeChallenge,
codeChallengeMethod: _codeChallengeMethod,
}: OAuthAuthorizeUrlParams): string | null
params — State, PKCE challenge (accepted but unused), and optional redirect URI.Returns: The Apple authorize URL, or null when OAUTH_APPLE_CLIENT_ID is unset.
getUserInfo(idToken)Apple does not expose a userinfo endpoint — the ID token is the user info. This helper takes an Apple-issued ID token, verifies its signature/issuer/audience/expiry, and returns the decoded claims.
function getUserInfo(idToken: string): Promise<AppleIdTokenClaims>
idToken — The Apple-issued ID token (JWT).Returns: The verified {@link AppleIdTokenClaims}.
jwkToPem(jwk)Convert a JWK to a PEM-encoded SPKI public key string suitable for
passing to jsonwebtoken.verify with algorithm RS256.
function jwkToPem(jwk: AppleJwk): string
jwk — The JWK to convert.Returns: A PEM-encoded SPKI public key.
refreshAccessToken(refreshToken)Exchanges a refresh token for a fresh access token (and possibly a new id_token).
function refreshAccessToken(refreshToken: string): Promise<AppleTokenResponse>
refreshToken — The refresh token previously issued by Apple.Returns: The token-refresh response.
resetJwksCache()Reset the JWKS cache. Exposed for tests; production code should not call this.
function resetJwksCache(): void
verify(code, _codeVerifier, redirectUri)Verifies an Apple OAuth authorization code and returns normalized
OAuthUserProps, or null when Apple affirmatively rejects the code
(HTTP 400 invalid_grant from /auth/token — invalid/expired/reused) so
the consumer surfaces a clean 403 instead of a misleading 500.
Note: Apple only includes the user's name in the initial form-post
callback (not in the ID token), so callers wanting display-name capture
must persist that value separately at the redirect-handler layer.
function verify(
code: string,
_codeVerifier?: string,
redirectUri?: string,
): Promise<{
username: string
email: string | undefined
emailVerified: boolean
oauthServer: 'apple'
oauthId: string
oauthData: {
[key: string]: unknown
iss: string
aud: string
sub: string
iat: number
exp: number
nonce?: string
nonce_supported?: boolean
email?: string
email_verified?: boolean | string
is_private_email?: boolean | string
real_user_status?: number
}
} | null>
code — The authorization code from the OAuth callback._codeVerifier — Unused; included for {@link OAuthVerifier} signature compatibility.redirectUri — The redirect URI used in the authorization request.Returns: Normalized OAuth user props, or null when Apple rejected the code.
verifyIdToken(idToken)Verifies an Apple ID token. Fetches Apple's JWKS, locates the key
matching the token's kid, verifies the RS256 signature, then asserts
iss === 'https://appleid.apple.com', aud === OAUTH_APPLE_CLIENT_ID,
and that the token has not expired.
function verifyIdToken(idToken: string): Promise<AppleIdTokenClaims>
idToken — The compact-serialized JWT to verify.Returns: The decoded and validated {@link AppleIdTokenClaims}.
APPLE_AUTHORIZATION_URLApple authorization endpoint.
const APPLE_AUTHORIZATION_URL: 'https://appleid.apple.com/auth/authorize'
APPLE_CLIENT_SECRET_DEFAULT_LIFETIME_SECONDSDefault client-secret JWT lifetime in seconds (5 minutes).
const APPLE_CLIENT_SECRET_DEFAULT_LIFETIME_SECONDS: 300
APPLE_CLIENT_SECRET_MAX_LIFETIME_SECONDSMaximum lifetime Apple permits for a client-secret JWT (6 months in seconds).
const APPLE_CLIENT_SECRET_MAX_LIFETIME_SECONDS: 15777000
APPLE_ID_TOKEN_ISSUERThe expected iss claim for Apple-issued ID tokens.
const APPLE_ID_TOKEN_ISSUER: 'https://appleid.apple.com'
APPLE_JWKS_URLApple JWKS endpoint URL.
const APPLE_JWKS_URL: 'https://appleid.apple.com/auth/keys'
APPLE_TOKEN_URLApple token endpoint.
const APPLE_TOKEN_URL: 'https://appleid.apple.com/auth/token'
DEFAULT_APPLE_SCOPEDefault scope when callers do not specify one.
const DEFAULT_APPLE_SCOPE: 'name email'
JWKS_CACHE_TTL_MSTTL for the cached JWKS, in milliseconds (1 hour).
const JWKS_CACHE_TTL_MS: number
oauthAppleSecretDefinitionsSecret definitions required by the Sign in with Apple OAuth bond.
const oauthAppleSecretDefinitions: SecretDefinition[]
serverNameThe OAuth server identifier for Apple.
const serverName: 'apple'
Implements @molecule/api-oauth interface.
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { serverName, verify, getAuthorizeUrl } from '@molecule/api-oauth-apple'
export function setupOauthApple(): void {
bond('oauth', serverName, { serverName, verify, getAuthorizeUrl })
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-http ^1.0.1@molecule/api-oauth ^1.0.1@molecule/api-secrets ^1.0.1OAUTH_APPLE_CLIENT_ID (required) — Apple Services ID
com.example.app.signinOAUTH_APPLE_TEAM_ID (required) — Apple Team ID
A1B2C3D4E5OAUTH_APPLE_KEY_ID (required) — Apple key ID
OAUTH_APPLE_PRIVATE_KEY (required) — Apple private key (.p8)
contents of AuthKey_ABC123DEF4.p8@molecule/api-bond@molecule/api-http@molecule/api-oauth@molecule/api-secretsjsonwebtokenIntegration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
state parameter is generated on initiation and
verified on callback (CSRF protection): a mismatched or absent state is
rejected (403); the redirect_uri is validated against an allowlist so an
attacker cannot redirect the code elsewhere; and the client secret + tokens
stay server-side — grep the browser bundle and network tab to confirm the
secret never reaches the client (only the authorize URL and returned code
cross the boundary).