Per aggiungere Sign in with Apple a un'app Capacitor, installa @capgo/capacitor-social-login, abilita la funzionalità Sign in with Apple sul tuo App ID e in Xcode, poi chiama SocialLogin.initialize({ apple: {} }), quindi chiama SocialLogin.login({ provider: 'apple' }). Su iOS questo apre il foglio nativo di Apple e restituisce un identity token. Android e web utilizzano il flusso OAuth web di Apple, che richiede un Services ID e un URL di redirect HTTPS.
Questa guida copre la configurazione di Apple Developer, il client code per iOS, Android e web, la verifica del token sul backend e gli errori più comuni. È pensata per Capacitor 8 e Xcode 26, che App Store Connect richiede per gli upload da aprile 2026.
Cosa ti serve prima di iniziare
| Requisito | Perché |
|---|---|
| Abbonamento a pagamento all'Apple Developer Program | Gli App ID, i Service ID e le chiavi di Sign in with Apple sono disponibili solo per i membri |
| Un Mac con Xcode 26 | Necessario per buildare e caricare app iOS nel 2026 |
| Progetto Capacitor 8 | Il plugin v8 punta a Capacitor 8 |
| Un dominio HTTPS che controlli | Solo per Android e web. Apple rifiuta localhost e URL di ritorno HTTP plain |
| Un endpoint backend | Sempre per verificare i token. Su Android riceve anche il callback di Apple |
Se non hai ancora i certificati di firma, il Generatore di certificati iOS li crea senza dover ballare con il keychain di Mac.
Come funziona Sign in with Apple su ogni piattaforma
Apple fornisce solo un SDK nativo per le proprie piattaforme, quindi il flusso è diverso:
| Piattaforma | Meccanismo | Client ID utilizzato | Backend richiesto per il login |
|---|---|---|---|
| iOS | Nativo ASAuthorizationAppleIDProvider sheet |
Bundle ID della tua app | No |
| Android | OAuth web Apple in una Chrome Custom Tab, Apple fa una POST al tuo server, il server reindirizza nell'app | ID dei servizi | Sì |
| Web | Popup JS di Apple | ID dei servizi | No (ma verifica il token) |
L'audience (aud) del token di identità segue il client ID. I token da iOS contengono il tuo bundle ID, quelli da Android e web contengono il Services ID. Il tuo backend deve accettare entrambi.
Passo 1: Configura l'account Apple Developer
Abilita la capability sul tuo App ID
- Apri Certificati, Identificatori e Profili e seleziona Identificatori.
- Apri l'App ID che corrisponde al tuo Capacitor
appId(ad esempiocom.example.app). Crealo se non esiste. - Sotto Funzionalità, seleziona Accedi con Apple e salva. Lascialo come App ID primario, a meno che tu non raggruppi più app.
Se modifichi le capabilities di un App ID esistente, rigenera i provisioning profile che lo usano. I vecchi profili non includono il nuovo entitlement.
Crea un Services ID (solo Android e web)
- In Identificatori, clicca + e seleziona ID dei servizi.
- Usa una descrizione come “Example Web Login” e un identificatore come
com.example.app.signin. Questo valore diventa ilclientIdsu Android e web. - Apri il nuovo Services ID, spunta Accedi con Apple, e clicca su Configura.
- Scegli il tuo App ID come App ID principale.
- Aggiungi il tuo dominio sotto Domini e sottodomini (senza schema, ad esempio
auth.example.com). - Aggiungi i Return URL sotto Return URL con tutto il
https://path completo, ad esempiohttps://auth.example.com/apple/callback. Per i login web, aggiungi l'URL della pagina che richiama il login, con e senza slash finale.
Crea una chiave Sign in with Apple (scambio token Android e server)
- Vai su Chiavi, clicca +, seleziona Accedi con Apple, e clicca su Configura per scegliere l'App ID principale.
- Registra la chiave e scarica il
.p8file. Apple ti consente di scaricarlo solo una volta. - Annota il Key ID e il tuo ID del Team (visualizzato in alto a destra nel developer portal e sotto Membership).
Il tuo server usa la chiave per firmare il client_secret JWT che invia all'endpoint token di Apple.
Registra le origini email se invii email
Gli utenti possono scegliere Nascondi il mio indirizzo email, il che ti fornisce un @privaterelay.appleid.com indirizzo. Apple inoltra solo le email da domini e indirizzi registrati sotto Services > Sign in with Apple for Email Communication. Se salti questo passaggio, le email di reset password e ricevute spariscono in silenzio.
Passo 2: Installa il plugin
bun add @capgo/capacitor-social-login
bunx cap sync
Il plugin include diversi provider. Se usi solo Apple (e forse Google), disattiva gli altri in capacitor.config.ts quindi i loro SDK non vengono distribuiti:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'Example',
webDir: 'dist',
plugins: {
SocialLogin: {
providers: {
apple: true,
google: false,
facebook: false,
twitter: false,
},
},
},
};
export default config;
Esegui bunx cap sync di nuovo dopo aver modificato questo blocco. Un provider disabilitato non è disponibile a runtime, quindi non disabilitare uno che chiami. Rimuovendo Facebook viene eliminato anche il AD_ID avvisi sui permessi che SDK genera in Google Play Console.
Passaggio 3: configurazione iOS
- Esegui
bunx cap open ios. - Seleziona la App target, apri Signing & Capabilities, clicca + Funzionalità, e aggiungi Accedi con Apple.
- Assicurati che il bundle identifier corrisponda all'App ID che hai configurato.
Xcode scrive com.apple.developer.applesignin all'interno App.entitlements. Se fai il build in CI o con Build Capgo, committa quel file di entitlements, altrimenti il cloud build produce un'app che fallisce con ASAuthorizationError code 1000.
Nessun Info.plist URL scheme o AppDelegate modifica è necessaria per Apple su iOS.
Passo 4: configurazione Android
Apple non ha un SDK per Android. Il plugin apre la pagina di autorizzazione di Apple in una Custom Tab con il tuo Services ID. Apple invia quindi il risultato come form POST al tuo URL di ritorno HTTPS. Il tuo server scambia il code, poi reindirizza a un deep link che riapre l'app con i token.
Aggiungi il deep link
In android/app/src/main/AndroidManifest.xml, all'interno dell' MainActivity <activity> elemento:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="com.example.app" android:host="apple-login" />
</intent-filter>
Inoltra l'intent al plugin
Modifica MainActivity.java in modo che il deep link raggiunga il plugin:
package com.example.app;
import android.content.Intent;
import android.net.Uri;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginHandle;
import ee.forgr.capacitor.social.login.SocialLoginPlugin;
public class MainActivity extends BridgeActivity {
@Override
protected void onNewIntent(Intent intent) {
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null
&& "apple-login".equals(data.getHost())) {
PluginHandle handle = getBridge().getPlugin("SocialLogin");
if (handle != null) {
Plugin plugin = handle.getInstance();
if (plugin instanceof SocialLoginPlugin) {
((SocialLoginPlugin) plugin).handleAppleLoginIntent(intent);
return;
}
}
}
super.onNewIntent(intent);
}
}
Il filtraggio sull'host mantiene funzionanti gli altri deep link (reset password, link di marketing).
L'endpoint di callback
Apple invia code, id_token, state, e al primo login un user stringa JSON al tuo URL di ritorno. L'endpoint deve scambiare il code e reindirizzare all'app con success=true più i token, che è il contratto che il plugin analizza. Un esempio minimo di Hono usando jose:
import { Hono } from 'hono';
import { SignJWT, importPKCS8, createRemoteJWKSet, jwtVerify } from 'jose';
import { saveAppleRefreshToken } from './db'; // your own persistence layer
const app = new Hono();
const TEAM_ID = process.env.APPLE_TEAM_ID!;
const KEY_ID = process.env.APPLE_KEY_ID!;
const SERVICE_ID = process.env.APPLE_SERVICE_ID!; // com.example.app.signin
const PRIVATE_KEY = process.env.APPLE_PRIVATE_KEY!; // contents of the .p8 file
const APP_REDIRECT = 'com.example.app://apple-login';
const APPLE_JWKS = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));
async function appleClientSecret() {
const key = await importPKCS8(PRIVATE_KEY, 'ES256');
return new SignJWT({})
.setProtectedHeader({ alg: 'ES256', kid: KEY_ID })
.setIssuer(TEAM_ID)
.setSubject(SERVICE_ID)
.setAudience('https://appleid.apple.com')
.setIssuedAt()
.setExpirationTime('5m')
.sign(key);
}
app.post('/apple/callback', async (c) => {
const form = await c.req.parseBody();
const code = String(form.code ?? '');
if (!code) {
return c.redirect(`${APP_REDIRECT}?success=false`);
}
// `form.user` is only present on the first authorization. Store it now.
const res = await fetch('https://appleid.apple.com/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: SERVICE_ID,
client_secret: await appleClientSecret(),
code,
grant_type: 'authorization_code',
redirect_uri: 'https://auth.example.com/apple/callback',
}),
});
if (!res.ok) {
return c.redirect(`${APP_REDIRECT}?success=false`);
}
const tokens = await res.json();
// Verify the identity token, then keep the long-lived refresh token on the server.
// Only the short-lived tokens go back to the app in the deep link.
const { payload } = await jwtVerify(tokens.id_token, APPLE_JWKS, {
issuer: 'https://appleid.apple.com',
audience: SERVICE_ID,
});
if (tokens.refresh_token) {
await saveAppleRefreshToken(String(payload.sub), tokens.refresh_token);
}
const params = new URLSearchParams({
success: 'true',
access_token: tokens.access_token,
id_token: tokens.id_token,
});
return c.redirect(`${APP_REDIRECT}?${params}`);
});
export default app;
Il plugin legge i token dalla query string del deep link, quindi tutto ciò che ci metti può finire nei log o essere letto da un'altra app che registra lo stesso custom scheme. Invia solo quelli a breve durata access_token e id_token, mai il refresh token, verifica la id_token signature and nonce sul tuo server prima di considerarlo affidabile, e usa uno scheme unico per la tua app (il tuo reverse bundle ID).
Il client_secret I JWT possono durare fino a sei mesi, ma generarne uno breve per ogni richiesta evita interruzioni dovute alla scadenza. Mantieni il .p8 file nel tuo secret manager, mai nel bundle dell'app.
Passaggio 5: Configurazione web
La build web usa Apple JS con il Services ID. Aggiungi l'URL esatto della pagina che attiva il login agli URL di ritorno del Services ID. Apple non accetta localhost, quindi testa il login web su un dominio di preview deployato o tramite un tunnel con un hostname reale.
Passaggio 6: Inizializzazione e login
Inizializza una volta all'avvio dell'app. Passa il redirectUrl solo su Android e web. Su iOS, un redirectUrl fa sì che il plugin invii il risultato a quell'URL tramite POST, cosa che non si desidera con il flusso nativo.
import { Capacitor } from '@capacitor/core';
import { SocialLogin } from '@capgo/capacitor-social-login';
const platform = Capacitor.getPlatform();
export async function initAuth() {
await SocialLogin.initialize({
apple: {
clientId: platform === 'ios' ? 'com.example.app' : 'com.example.app.signin',
redirectUrl:
platform === 'android'
? 'https://auth.example.com/apple/callback'
: platform === 'web'
? 'https://app.example.com/login'
: '',
},
});
}
Poi avvia il login da un pulsante. Usa un nonce casuale così che il tuo server possa rifiutare i token riutilizzati:
import { SocialLogin } from '@capgo/capacitor-social-login';
export async function signInWithApple() {
const nonce = crypto.randomUUID();
try {
const { result } = await SocialLogin.login({
provider: 'apple',
options: {
scopes: ['email', 'name'],
nonce,
},
});
if (!result.idToken) {
throw new Error('Apple did not return an identity token');
}
// Send to your backend. Name and email are only present on the first login.
const response = await fetch('https://api.example.com/auth/apple', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idToken: result.idToken,
nonce,
givenName: result.profile.givenName,
familyName: result.profile.familyName,
}),
});
return response.json();
} catch (error: any) {
if (error?.code === 'USER_CANCELLED') {
return null; // user closed the sheet, not a failure
}
throw error;
}
}
La risposta ti fornisce:
| Campo | Contenuto |
|---|---|
result.idToken |
JWT firmato da Apple, quello che il tuo server verifica |
result.profile.user |
ID utente stabile di Apple (lo stesso del sub claim) |
result.profile.email |
Email o indirizzo relay privato, se concesso |
result.profile.givenName / familyName |
Solo alla prima autorizzazione |
result.accessToken |
Con le impostazioni predefinite, questo contiene l'autorizzazione code, non un vero token di accesso |
result.authorizationCode |
Presente quando inizializzi con useProperTokenExchange: true |
Se il tuo server ha bisogno dei refresh token di Apple (ad esempio per chiamare l'endpoint di revoca quando un utente elimina il proprio account), imposta useProperTokenExchange: true nel apple config e scambia authorizationCode sul server.
Passaggio 7: Verifica l'identity token sul tuo backend
Non fidarti mai dei campi del profilo inviati dal client. Verifica la firma JWT con le chiavi pubbliche di Apple e controlla i claim:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const appleKeys = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));
export async function verifyAppleToken(idToken: string, expectedNonce: string) {
const { payload } = await jwtVerify(idToken, appleKeys, {
issuer: 'https://appleid.apple.com',
audience: ['com.example.app', 'com.example.app.signin'], // bundle ID + Services ID
});
if (payload.nonce !== expectedNonce) {
throw new Error('Nonce mismatch');
}
return {
appleUserId: payload.sub as string,
email: payload.email as string | undefined,
emailVerified: payload.email_verified === true || payload.email_verified === 'true',
};
}
Usa come chiave il record utente con sub, non tramite email. L'email può essere un indirizzo relay e gli utenti possono smettere di condividerlo. Dopo la verifica, emetti il tuo token di sessione. Se usi Supabase o Firebase, verificano il token per te, vedi Supabase con Capacitor Social Login.
Regole dell'App Store da tenere presente
- Linea guida 4.8: se offri Google, Facebook o un altro login di terze parti come sistema di account principale, devi anche offrire un'opzione equivalente focalizzata sulla privacy. Sign in with Apple è idoneo.
- Linea guida 5.1.1(v): le app che permettono agli utenti di creare account devono consentire la cancellazione dell'account all'interno dell'app. Per gli utenti di Sign in with Apple, chiama l'
https://appleid.apple.com/auth/revokein caso di eliminazione. Consulta la nostra guida all'eliminazione dell'account. - Design del pulsante: usa gli stili di pulsante approvati da Apple e rendi il pulsante almeno così prominente come gli altri pulsanti di login.
Risoluzione problemi
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
ASAuthorizationError code 1000 su iOS |
Capability o entitlement mancante nel target compilato | Aggiungi Sign in with Apple in Xcode, fai il commit App.entitlements, rigenera i profili |
| Code 1001 su iOS | Annullato dall'utente | Il plugin fallisce con USER_CANCELLED, trattalo come non un errore |
invalid_client su Android o web |
Errato clientId (bundle ID usato al posto di Services ID) o errato client_secret |
Usa il Services ID, controlla Team ID, Key ID e il contenuto della chiave |
invalid_request: Invalid redirect_uri |
URL di ritorno non registrata esattamente | Aggiungi l'intero https:// URL all'ID dei servizi, fai corrispondere le barre finali |
| L'app non torna dopo il login su Android | Lo scheme o l'host dell'Intent filter differisce dal redirect del server | Confronta com.example.app://apple-login carattere per carattere |
apple.android.redirectUrl is null or empty |
No redirectUrl su Android |
Passa l'URL di callback HTTPS durante l'inizializzazione su Android |
Nome ed email sono null |
Non è la prima autorizzazione | Leggili dal tuo database. Revoca l'app nelle impostazioni del dispositivo per riprovare |
Token rifiutato dal backend: aud discrepanza |
Il server accetta una sola audience | Accetta sia il bundle ID che il Services ID |
Rilasciare fix dopo la release
La maggior parte dei bug di auth sta nel codice TypeScript: un client ID sbagliato per piattaforma, un nonce mancante, una gestione degli errori scadente. Puoi inviarli agli utenti lo stesso giorno con Live updates di Capgo senza aspettare l'App Review. Le modifiche agli entitlements, all'Android manifest o MainActivity sono native e richiedono una nuova build per lo store.
Cosa fare ora
- Aggiungi Google accanto ad Apple: Come effettuare il login con Google usando Capacitor.
- Scegli il mix di metodi ideale per la tua app: Come aggiungere l'autenticazione a un'app Capacitor.
- Riferimenti plugin e pagine per piattaforma: Docs Social Login e il Pagina di configurazione Apple iOS.