Documentation

SSO

Apple

Add Sign in with Apple to VitNode. Create a Services ID and key, register the HTTPS return URL and wire up the Apple SSO adapter.

Add a Continue with Apple button to /login and /register. Visitors can share their real email or a private relay address, and both work with VitNode.

Before you begin

Set up Sign in with Apple

Create an App ID

In Certificates, Identifiers & Profiles, add an App IDs identifier (type App), give it a bundle ID such as com.example.forum, and tick Sign in with Apple under capabilities. This is the primary App ID the web sign-in groups under.

Create a Services ID

Add another identifier, this time of type Services IDs. Its identifier, for example com.example.forum.web, is the client ID VitNode sends to Apple. The description is what visitors see on Apple's consent screen.

Register the domain and return URL

Open the new Services ID, tick Sign in with Apple and press Configure. Choose the primary App ID, then add:

FieldValue
Domainsyour-domain.com
Return URLshttps://your-domain.com/login/sso/apple

VitNode builds the return URL from VITNODE_WEB_URL as {VITNODE_WEB_URL}/login/sso/apple, so they must match exactly: same scheme and host, no trailing slash.

Create a key

Under Keys, add a key, tick Sign in with Apple, configure it with the same primary App ID and register it. Download the .p8 file and note the 10-character Key ID. Apple lets you download the file once. Your 10-character Team ID is under Membership details in your developer account.

Set the environment variables

Keep these server-side and out of clientEnv. Paste the .p8 contents with \n for line breaks; VitNode turns them back into newlines.

.env
APPLE_CLIENT_ID=com.example.forum.web
APPLE_TEAM_ID=A1B2C3D4E5
APPLE_KEY_ID=F6G7H8I9J0
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGT...\n-----END PRIVATE KEY-----"
VITNODE_WEB_URL=https://your-domain.com

Apple's token endpoint wants a client secret: a short ES256 JWT signed with the key. AppleSSOApiPlugin creates it for you, keeps it for 24 hours and renews it before it expires, so there is no secret to rotate by hand.

Add the adapter

Add AppleSSOApiPlugin to authorization.ssoAdapters in the API's vitnode.api.config.ts (where it lives), next to any adapters you already have. Keep the rest of the config as it is:

src/vitnode.api.config.ts (excerpt)
import { AppleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/apple'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  // ...
  authorization: {
    ssoAdapters: [
      // ...
      AppleSSOApiPlugin({
        clientId: process.env.APPLE_CLIENT_ID,
        teamId: process.env.APPLE_TEAM_ID,
        keyId: process.env.APPLE_KEY_ID,
        privateKey: process.env.APPLE_PRIVATE_KEY,
      }),
    ],
  },
})

No authorization block yet? Create one with just ssoAdapters, as in the SSO setup. Then restart the dev server, since .env and the API config are only read at start-up.

Check the result

Open /login on your HTTPS domain and press the Apple button under Continue with. Apple asks whether to share or hide the email, then returns to /login/sso/apple and you land signed in.

How Apple's callback works

Because VitNode asks for the name and email, Apple returns with a cross-site POST (response_mode=form_post), and browsers do not send VitNode's SameSite=Lax sign-in cookie with it. VitNode's request middleware (part of createVitNodeStart) answers that POST with a 303 redirect to the same URL as a normal GET, which carries the cookie. Nothing is relaxed. The state is still checked against the browser that started the sign-in. Only code, state, error and the name from Apple's user field are forwarded.

Apple sends the name only on the first authorization. VitNode uses it as the new account's display name (or "Apple user" when it is hidden). A later import from Connected accounts finds no name, because Apple does not send it again.

Develop locally

Apple will not redirect to localhost. Expose the dev server through an HTTPS tunnel (for example Cloudflare Tunnel or ngrok) and use that host everywhere:

.env
VITNODE_WEB_URL=https://forum-dev.example.com
VITNODE_API_URL=https://forum-dev.example.com

Add forum-dev.example.com to the Services ID's domains and https://forum-dev.example.com/login/sso/apple to its return URLs.

Scopes and profile fields

SettingValue
Scopesname email
Account IDID token sub (stable per developer team)
Email trustedwhen the ID token says email_verified: true
Profile fieldsfirst name, last name (sent on the first authorization only)
ID tokenRS256 signature, issuer, audience, expiry and nonce validated

Private relay addresses (...@privaterelay.appleid.com) are real, verified inboxes. To email them, register your sending domain under Sign in with Apple for Email Communication in the developer account.

Troubleshooting

invalid_client

The Services ID, Team ID, Key ID or key do not belong together. Check that the key has Sign in with Apple enabled for the same primary App ID as the Services ID, and that APPLE_CLIENT_ID is the Services ID, not the App ID.

  • "Invalid redirect_uri" on Apple's page. The return URL is not listed on the Services ID, or VITNODE_WEB_URL differs from it.
  • The callback page says the request expired. The sign-in started on another host than it returned to (for example localhost vs. the tunnel), so the browser had no matching cookie.
  • A missing variable. The button still renders. A missing APPLE_CLIENT_ID fails as soon as it is pressed; a missing team ID, key ID or private key fails only after Apple's screen, when VitNode exchanges the code. Either way the server log shows Missing Apple ....