Skip to content
App Signing & PushPart 4 of 44
App SigningMobile DevelopmentFlutter

Sign in with Apple on Android and the Web: Services ID and Private Key

Sign in with Apple on Android and the web needs a Services ID, return URLs and a private key that signs a six-month client secret. Setup for Firebase, Supabase and your server.

By Bimal Khatri·16 min read·Sep 17, 2026·Updated Sep 17, 2026
Sign in with Apple on Android and the Web: Services ID and Private Key

Sign in with Apple on Android or a website runs as a web page on Apple's own site, and Apple needs four things from you before it will work: a primary App ID with Sign in with Apple turned on, a Services ID that names your website, the domains and return URLs Apple may send people back to, and a Sign in with Apple private key (a .p8) that your server uses to prove it is you.

Your server uses that key to sign a client secret, a token that lasts at most six months. Firebase Authentication takes the key itself. Supabase asks you for a finished secret, so you have to make a new one before it expires.

The first section explains the flow in everyday terms. The rest walks through the Apple setup, then what Firebase, Supabase and a hand-built server each ask for.

How it works, in everyday terms

Picture a members' club that lets people in on the word of a trusted doorman.

  • Apple is the doorman. People prove who they are to Apple, never to you, and Apple tells you the result.
  • The Services ID is your club's entry in Apple's register. It says which website is asking, and which addresses Apple may send people back to afterwards.
  • The client secret is a signed letter from your club to Apple, sealed with your private key. Apple swaps the one-time code for that person's tokens only when the request carries a valid letter.

The analogy breaks at one point: the letter has an expiry date you choose, up to six months ahead, and once it passes, Apple turns your server away even though the key that signed it still works. That detail is behind sign-ins that work for months and then stop.

On an iPhone, iPad or Mac, an app signs people in through Apple's AuthenticationServices framework. Other platforms do not have that framework. Apple's guidance for them, Android included, is the web flow: Sign in with Apple JS, or the same requests made by hand in a browser tab.

A sequence chart of the web flow. The browser opens Apple's authorize page with the Services ID and a return URL. The user signs in and approves on Apple's page. Apple posts a one-time code to your return URL. Your server signs a client secret with the .p8, then sends the code and the secret to Apple's token endpoint. Apple returns an identity token, an access token and a refresh token. Your server signs the user in and, on Android, sends them back to the app.

What Apple needs from you

PieceExampleWhat it does
Primary App IDcom.example.app, with Sign in with Apple enabledThe app everything else is grouped under. Users consent once per group
Services IDcom.example.app.webIdentifies your website or non-Apple app. It is the client_id in the web flow
Domains and return URLsexample.com, https://example.com/auth/apple/callbackWhere Apple may send the result
Sign in with Apple keyAuthKey_ABC123DEFG.p8, Key ID ABC123DEFGSigns the client secret. Linked to the primary App ID
Team IDA1B2C3D4E5Goes into the client secret as its issuer

A map of how the pieces relate inside one developer team. The Services ID, which carries your domains and return URLs, is configured against the primary App ID. The Sign in with Apple private key is associated with the same primary App ID. Other App IDs, such as a Mac version of the app, can be grouped with the primary so users consent only once.

Everything below is done in Certificates, Identifiers & Profiles in your developer account, and each of these steps needs the Account Holder or Admin role.

Step 1: a primary App ID

Open the App ID of your iOS or Mac app, tick Sign in with Apple, and enable it as a primary App ID. You can also turn the capability on from Xcode.

If you have several related apps, say an iPhone app and a Mac app, enable one as the primary and group the others with it. Apple's reason for grouping is consent: a person approves sharing their details once for the whole group of apps and websites. An App ID that joined by grouping cannot itself become the anchor for further grouping; Apple says it would need to be ungrouped and enabled as a primary first.

A note on wording. Apple's configuration guide says that to authenticate users on the web you must have an existing app in the App Store that uses Sign in with Apple. The portal step for the web asks you to pick a primary App ID with Sign in with Apple enabled. Plan for both.

Step 2: the Services ID, domains and return URLs

A Services ID identifies a website that uses Apple's web services, Sign in with Apple among them. Register one under Identifiers with a description and a reverse-domain identifier such as com.example.app.web.

Then open the Services ID, tick Sign in with Apple, and choose Configure:

  1. Pick the primary App ID from step 1.
  2. Enter your domains, subdomains and return URLs. Apple's help page describes them as one comma-separated list, and at least one domain or subdomain is required.
  3. Save, review and confirm.

Apple's rules for return URLs (Apple also calls them redirect URIs):

  • Absolute, with scheme, host and path, such as https://example.com/path/to/endpoint.
  • A real domain name. Apple's authorization request does not accept an IP address or localhost, which is why local testing usually needs a tunnel or a staging domain.
  • No fragment (nothing after a #).
  • Organisations can register up to 100 website URLs per Services ID; individual members up to 10.

Apple's help page adds that registering a domain or subdomain does not require uploading any file to your server.

Each web property you run gets its own Services ID. Apple's documentation describes one Services ID per web service, each configured with its own domains and return URLs.

Step 3: the Sign in with Apple key

Under Keys, add a key, tick Sign in with Apple, configure it with your primary App ID, and download the .p8. Note the Key ID shown under the key's name.

The rules that follow from how Apple ties keys to apps:

  • Two keys per primary App ID, at most. That is enough to rotate without a gap.
  • One download. Apple does not keep the file. If it is lost, make another key.
  • Rotate by overlap. Apple's advice for a compromised key: create a new key for the same primary App ID, move to it, then revoke the old one.

The difference between this key and other Apple keys is covered in p8 vs p12. In short, the .p8 has no password, so it belongs in a secret manager, never in an app or a repository. An Android app never needs it: only the server, or Firebase or Supabase, does.

Step 4: the client secret

When your server trades a sign-in code for tokens, Apple wants a client_secret. It is not a fixed string from the portal. It is a JSON Web Token (JWT) you create, signed with the key from step 3.

PartFieldValue
HeaderalgES256
HeaderkidYour Key ID, ABC123DEFG
PayloadissYour Team ID, A1B2C3D4E5
PayloadiatWhen you made it, in Unix seconds
PayloadexpWhen it expires. No more than 15,777,000 seconds (six months) ahead, by Apple's clock
Payloadaudhttps://appleid.apple.com
PayloadsubThe same identifier you send as client_id: your Services ID on the web and Android. Case-sensitive

Apple asks for ECDSA on the P-256 curve with SHA-256, which is what ES256 means. Here is a complete generator in Node.js with no extra packages. It ran for this post on Node 24 with a throwaway key, and the result verified against that key's public half:

import { readFileSync } from "node:fs";
import { createPrivateKey, sign } from "node:crypto";

const teamId = "A1B2C3D4E5";
const keyId = "ABC123DEFG";
const servicesId = "com.example.app.web"; // the client_id your sign-in uses
const privateKey = createPrivateKey(readFileSync(`AuthKey_${keyId}.p8`));

const now = Math.floor(Date.now() / 1000);
const header = { alg: "ES256", kid: keyId };
const payload = {
  iss: teamId,
  iat: now,
  exp: now + 180 * 24 * 60 * 60, // 180 days: under Apple's 15,777,000-second limit
  aud: "https://appleid.apple.com",
  sub: servicesId,
};

const encode = (part) => Buffer.from(JSON.stringify(part)).toString("base64url");
const unsigned = `${encode(header)}.${encode(payload)}`;
const signature = sign("sha256", Buffer.from(unsigned), {
  key: privateKey,
  dsaEncoding: "ieee-p1363",
}).toString("base64url");

console.log(`${unsigned}.${signature}`);

A left-to-right flow of the client secret's life. You sign the secret with the .p8, give it to the server or provider that talks to Apple, and it works for up to six months. After its expiry Apple answers token requests with invalid_client until a new secret is signed.

A server that holds the .p8 can sign a fresh secret whenever it likes, even for every request, and never think about expiry. A service that stores a secret you pasted in cannot, and that is where the six-month limit bites.

Handing the pieces to Firebase, Supabase or your own server

A comparison of three setups. Firebase Authentication registers its own auth handler as the return URL, takes the Services ID, and takes the Team ID, Key ID and private key in its OAuth code flow settings, so no ready-made secret is asked for. Supabase registers the project's auth callback URL, takes the Services ID as the first of its Client IDs, and takes a client secret you generate, which must be renewed within six months. Your own server uses any HTTPS return URL you control, sends the Services ID as client_id, and signs its own secret.

Firebase Authentication

Firebase's Android, iOS and web guides ask for the same Apple setup:

  1. Configure the Services ID as above, and register this return URL:

    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
  2. Create the Sign in with Apple key.

  3. In the Firebase console, open Authentication, then the Sign-in method tab, and enable Apple. Enter the Service ID, and under OAuth code flow configuration enter your Apple Team ID, the Key ID and the private key.

Firebase asks for the private key itself rather than a finished client secret, and its guides say nothing about renewing one.

On Android, two more things apply. Firebase's guide asks for your app's SHA-1 fingerprint in the project settings, and the sign-in runs through the SDK's OAuth provider with the provider ID apple.com, which opens Apple's page in a Custom Chrome Tab. If your fingerprints are not set up yet, getting SHA-1 fingerprints for all three Android keys covers it. In Flutter, the firebase_auth plugin uses AppleAuthProvider with signInWithProvider on mobile and signInWithPopup on the web.

Firebase's guides also note three things that are easy to miss:

  • People signing in need an Apple Account with two-factor authentication, signed in to iCloud on an Apple device. That applies to your testers too.
  • If Firebase sends emails (email verification, sign-in links), register noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com with Apple's private email relay, or users who hid their address will not receive them.
  • To let users delete their account and revoke Apple's tokens, the Services ID and OAuth code flow settings must be filled in even for iOS-only apps, and the user has to sign in again first, because Firebase does not store Apple's tokens.

Supabase

Supabase's Apple guide splits the work in two. Native sign-in on Apple platforms needs only your App IDs. The OAuth flow, which is what web, Flutter web and non-iOS Kotlin apps use, needs the full setup:

  1. A Services ID whose domain is your project's Supabase host (usually YOUR_PROJECT_REF.supabase.co) and whose return URL is:

    https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
  2. The Sign in with Apple key, used to generate a client secret. Supabase's docs include a generator that runs in the browser, and a script like the one above does the same job.

  3. In the dashboard's Apple provider settings, add the Services ID to Client IDs and paste the generated secret.

Two Supabase rules matter:

  • Order of Client IDs. If you also list native App IDs, the Services ID must come first. Supabase uses the first entry for the web OAuth flow and accepts any entry as the audience of a native ID token. With an App ID first, native sign-in works and web sign-in is rejected by Apple.
  • Six-month renewal. Supabase's guide says Apple requires a new secret every six months for the OAuth flow, and recommends a calendar reminder. Native-only projects do not need one.

The same settings can be made through Supabase's Management API, whose fields are external_apple_enabled, external_apple_client_id and external_apple_secret. Supabase also notes that it does not support Apple's server-to-server notification endpoint, and that Apple provides a user's full name only on their first sign-in, so the app has to save it then.

Your own server

Without a provider, your server makes the two requests itself. First, send the browser to Apple's authorize page:

https://appleid.apple.com/auth/authorize
  ?client_id=com.example.app.web
  &redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fapple%2Fcallback
  &response_type=code
  &scope=name%20email
  &response_mode=form_post
  &state=RANDOM_STATE
  &nonce=RANDOM_NONCE

(Shown on several lines for reading. It is one URL.) The parameters, according to Apple:

ParameterWhat Apple requires
client_idYour Services ID, without the Team ID
redirect_uriOne of the registered return URLs
response_typecode, or both code and id_token. id_token on its own is not supported
scopename, email, both or neither, space-separated
response_modequery, fragment or form_post. Must be form_post if you request any scope
state, nonceYour values, to tie the response to the session and stop replays

Apple then posts the result to your return URL: a one-time code that is valid for five minutes, the state you sent, an id_token if you asked for one, and a user field with the details your scopes requested. If the person cancels, the only error Apple sends there is user_cancelled_authorize.

Second, exchange the code:

curl -X POST "https://appleid.apple.com/auth/token" \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'client_id=com.example.app.web' \
  -d 'client_secret=CLIENT_SECRET' \
  -d 'code=CODE' \
  -d 'grant_type=authorization_code' \
  --data-urlencode 'redirect_uri=https://example.com/auth/apple/callback'

Include redirect_uri only if you sent one in the first request. A successful answer has an id_token, an access_token and a refresh_token. Later, the same endpoint takes grant_type=refresh_token to check that the session is still valid.

On Android, Apple's guide describes the ending: platforms that rely on custom URL schemes, Android among them, must handle the result on the server at the redirect_uri endpoint, then redirect to the app's custom URL scheme to hand control back.

The private email relay

A person can choose to hide their email address. Your app then receives an address at privaterelay.appleid.com, and Apple forwards mail to the real inbox, but only from senders you registered.

To send to those addresses, register your outbound email domains, subdomains or addresses in your developer account. Organisations can register up to 100 sources and individuals up to 32. Apple asks you to authenticate the domains with SPF, DKIM or, preferably, both. Apple also offers an optional server-to-server notification endpoint (one URL per app group, TLS 1.2 or later) that tells you when a user changes email forwarding, deletes their account with your app, or deletes their Apple Account.

Why it fails

What you seeLikely cause
invalid_client from the token endpointApple lists: a mismatched or invalid client ID, an invalid client secret (expired, malformed claims or a bad signature), or a mismatched or invalid redirect URI
Web sign-in worked for months, then invalid_client everywhereThe client secret passed its exp. Sign a new one
invalid_grant, "The code has expired or has been revoked."The code is over five minutes old, was already used, or belongs to a different client ID
invalid_requestA parameter is missing or unsupported, or the request carries more than one credential
The authorize request is refused before anyone can sign inCheck that the return URL is registered exactly, is not localhost or an IP address, and has no fragment
Name or email never arrivesScopes need response_mode=form_post. Supabase also notes that Apple sends the full name only on the first sign-in, so an app that did not save it then will not see it again
client_id and sub disagreeThe secret was signed for the App ID while the web flow sends the Services ID, or the other way round. They must match, including case
Supabase: native works, web failsAn App ID is listed before the Services ID in Client IDs
Firebase sign-in fails on Android onlyCheck that the app's SHA-1 fingerprint is in the Firebase project, which Firebase's Android guide requires
Sign-ins stopped for everyone at onceThe key was revoked (Apple says revoking affects every service that uses it), or a stored secret expired

Questions people ask

Can I use Sign in with Apple on Android?

Yes, through the web flow. Apple points other platforms, Android included, to Sign in with Apple JS or the same requests made by hand in a browser tab. Firebase and Supabase both handle this for you once the Services ID and key are configured.

What is a Services ID in Apple's developer portal?

An identifier for a website, or an app on a non-Apple platform, that uses Apple web services such as Sign in with Apple. You configure it with your domains and return URLs, link it to a primary App ID, and use it as the client_id.

How long does the Sign in with Apple client secret last?

As long as its exp claim says, which may be no more than 15,777,000 seconds (six months) after Apple's current time. After that, token requests fail with invalid_client.

Does the Sign in with Apple key expire?

No. The .p8 keeps working until you revoke it. Only the client secrets you sign with it expire.

How many Sign in with Apple keys can I have?

Two per primary App ID, which leaves room to create a replacement before revoking the old key.

Do I need a Services ID for an iOS-only app?

Not for native sign-in itself. Firebase still asks for the Services ID and key settings if you want to revoke Apple tokens when a user deletes their account.

Why do I get invalid_client?

Apple lists three causes: a wrong client ID, a bad client secret (expired, malformed claims or an invalid signature), or a wrong redirect URI. An expired secret is the one that appears months after a working setup. Also check that the secret's sub matches the client_id exactly, that iss is your Team ID and that kid is your Key ID.

Can I test Sign in with Apple on localhost?

Not directly. Apple's authorization request needs a return URL with a domain name, not localhost or an IP address, so use a staging domain or a tunnel with a real hostname.

Keep reading

More writing

Keep reading