Salta al contenuto principale

Come accedere con Apple usando Capacitor (2026)

Aggiungi Sign in with Apple a un'app Capacitor 8 su iOS, Android e web: configurazione Apple Developer, plugin code, controlli dei token lato backend ed errori comuni.

Pulsante Sign in with Apple in un'app Capacitor con il plugin Capgo Social Login

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

  1. Apri Certificati, Identificatori e Profili e seleziona Identificatori.
  2. Apri l'App ID che corrisponde al tuo Capacitor appId (ad esempio com.example.app). Crealo se non esiste.
  3. 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)

  1. In Identificatori, clicca + e seleziona ID dei servizi.
  2. Usa una descrizione come “Example Web Login” e un identificatore come com.example.app.signin. Questo valore diventa il clientId su Android e web.
  3. Apri il nuovo Services ID, spunta Accedi con Apple, e clicca su Configura.
  4. Scegli il tuo App ID come App ID principale.
  5. Aggiungi il tuo dominio sotto Domini e sottodomini (senza schema, ad esempio auth.example.com).
  6. Aggiungi i Return URL sotto Return URL con tutto il https:// path completo, ad esempio https://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)

  1. Vai su Chiavi, clicca +, seleziona Accedi con Apple, e clicca su Configura per scegliere l'App ID principale.
  2. Registra la chiave e scarica il .p8 file. Apple ti consente di scaricarlo solo una volta.
  3. 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

  1. Esegui bunx cap open ios.
  2. Seleziona la App target, apri Signing & Capabilities, clicca + Funzionalità, e aggiungi Accedi con Apple.
  3. 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.

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/revoke in 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

Live updates per le app Capacitor

Quando un bug nel web-layer è live, rilascia la fix tramite Capgo invece di aspettare giorni l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale processo di review.

Ultimi articoli dal blog

Capgo ti offre gli insight migliori che ti servono per creare un'app mobile davvero professionale.