Pour ajouter Sign in with Apple à une application Capacitor, installez @capgo/capacitor-social-login, activez la capacité Sign in with Apple sur votre App ID et dans Xcode, puis appelez SocialLogin.initialize({ apple: {} }), puis appelez SocialLogin.login({ provider: 'apple' }). Sur iOS, cela ouvre la feuille native Apple et renvoie un token d'identité. Android et le web utilisent le flux OAuth web d'Apple, qui nécessite un Services ID et une URL de redirection HTTPS.
Ce guide couvre la configuration Apple Developer, le client code pour iOS, Android et le web, la vérification du token sur votre backend, ainsi que les erreurs les plus courantes. Il cible Capacitor 8 et Xcode 26, requis par App Store Connect pour les uploads depuis avril 2026.
Ce dont vous avez besoin avant de commencer
| Exigence | Pourquoi |
|---|---|
| Un abonnement payant au programme Apple Developer | Les App IDs, Services IDs et clés Sign in with Apple sont réservés aux membres |
| Un Mac avec Xcode 26 | Nécessaire pour compiler et envoyer des apps iOS en 2026 |
| Projet Capacitor 8 | Le plugin v8 cible Capacitor 8 |
| Un domaine HTTPS que vous contrôlez | Uniquement pour Android et le web. Apple refuse localhost et les URL de retour en HTTP simple |
| Un endpoint backend | Toujours pour vérifier les tokens. Sur Android, il reçoit aussi le callback d'Apple |
Si vous ne disposez pas encore de certificats de signature, le Générateur de certificats iOS les crée sans avoir à manipuler le trousseau Mac.
Comment fonctionne Sign in with Apple sur chaque plateforme
Apple ne fournit un SDK natif que pour ses propres plateformes, le flux diffère donc :
| Plateforme | Mécanisme | Client ID utilisé | Backend requis pour la connexion |
|---|---|---|---|
| iOS | Natif ASAuthorizationAppleIDProvider feuille |
Bundle ID de votre application | Non |
| Android | OAuth web Apple dans un Chrome Custom Tab : Apple envoie une requête POST à votre serveur, puis le serveur redirige vers l’application | ID de service | Oui |
| Le Web | Popup JS d'Apple | ID de service | Non (mais vérifier le token) |
L'audience (aud) du token d'identité suit le client ID. Les tokens issus d'iOS contiennent votre bundle ID, ceux d'Android et du web contiennent le Services ID. Votre backend doit accepter les deux.
Étape 1 : Configurer le compte Apple Developer
Activer la capacité sur votre App ID
- Ouvrez Certificats, identifiants et profils et sélectionnez Identifiants.
- Ouvrez l’App ID correspondant à votre Capacitor
appId(par exemplecom.example.app). Créez-le s’il n’existe pas. - Inférieur à Capacités, cochez Se connecter avec Apple et enregistrez. Laissez-l’en App ID principal, sauf si vous regroupez plusieurs apps.
Si vous modifiez les capacités d’un App ID existant, régénérez les profils de provisionnement qui l’utilisent. Les anciens profils n’incluent pas la nouvelle autorisation.
Créer un Services ID (Android et web uniquement)
- Dans Identifiants, cliquez sur + et choisissez Identifiants de service.
- Utilisez une description du type « Exemple de connexion web » et un identifiant du type
com.example.app.signin. Cette valeur devient leclientIdsur Android et le web. - Ouvrez le nouveau Services ID, cochez Se connecter avec Apple, et cliquez sur Configurer.
- Sélectionnez votre App ID comme App ID principal.
- Ajoutez votre domaine sous Domaines et sous-domaines (sans schéma, par exemple
auth.example.com). - Ajoutez les URL de retour sous URL de retour avec le chemin complet
https://, par exemplehttps://auth.example.com/apple/callback. Pour les connexions web, ajoutez l’URL de la page qui déclenche la connexion, avec et sans slash final.
Créer une clé Sign in with Apple (Android et échange de token côté serveur)
- Accéder à Clés, cliquez sur +, cochez Se connecter avec Apple, et cliquez sur Configurer pour choisir l'App ID principal.
- Enregistrez la clé et téléchargez le
.p8fichier. Apple ne permet de le télécharger qu'une seule fois. - Notez le ID de clé et votre ID d'équipe (affiché en haut à droite du portail développeur et dans la section Adhésion).
Votre serveur utilise la clé pour signer le client_secret JWT qu'il envoie à l'endpoint de token d'Apple.
Enregistrez les sources d'e-mail si vous envoyez des e-mails
Les utilisateurs peuvent choisir Masquer mon e-mail, ce qui vous donne une @privaterelay.appleid.com adresse. Apple ne transfère que les e-mails provenant des domaines et adresses que vous enregistrez sous Services > Sign in with Apple pour la communication par e-mail. Si vous l’ignorez, vos e-mails de réinitialisation de mot de passe et vos reçus disparaîtront sans prévenir.
Étape 2 : Installer le plugin
bun add @capgo/capacitor-social-login
bunx cap sync
Le plugin regroupe plusieurs providers. Si vous n'utilisez qu'Apple (et peut-être Google), désactivez les autres dans capacitor.config.ts donc leurs SDK ne sont pas déployés :
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;
Exécutez bunx cap sync à nouveau après avoir modifié ce bloc. Un provider désactivé est indisponible au runtime, ne désactivez donc pas celui que vous appelez. Supprimer Facebook supprime également le AD_ID les avertissements de permissions que son SDK déclenche dans Google Play Console.
Étape 3 : configuration iOS
- Exécutez
bunx cap open ios. - Sélectionnez l’ cible App, ouvrez Signature et capacités, cliquez sur + Capacité, et ajouter Se connecter avec Apple.
- Vérifiez que l’identifiant de bundle correspond à l’App ID que vous avez configuré.
Xcode écrit com.apple.developer.applesignin en App.entitlements. Si vous compilez en CI ou avec Build Capgo, committez ce fichier entitlements, sinon le build cloud génère une app qui échoue avec ASAuthorizationError code 1000.
Non Info.plist schéma d'URL ou AppDelegate changement n'est nécessaire pour Apple sur iOS.
Étape 4 : configuration Android
Apple n'a pas de SDK Android. Le plugin ouvre la page d'autorisation d'Apple dans un Custom Tab avec votre Services ID. Apple envoie ensuite le résultat via un POST de formulaire à votre URL de retour HTTPS. Votre serveur échange le code, puis redirige vers un deep link qui rouvre l'app avec les tokens.
Ajouter le deep link
Dans android/app/src/main/AndroidManifest.xml, à l'intérieur de l' MainActivity <activity> élément :
<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>
Transmettez l'intent au plugin
Modifier MainActivity.java pour que le deep link atteigne le 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);
}
}
Le filtrage sur l'hôte préserve vos autres deep links (réinitialisation de mot de passe, liens marketing).
L'endpoint de callback
Apple publie code, id_token, state, et lors de la première connexion, un user chaîne JSON vers votre URL de retour. L'endpoint doit échanger le code et rediriger vers l'app avec success=true ainsi que les tokens, qui constituent le contrat analysé par le plugin. Un exemple minimal avec Hono utilisant 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;
Le plugin lit les tokens depuis la query string du deep link, donc tout ce que vous y mettez peut finir dans les logs ou être lu par une autre app qui enregistre le même schéma personnalisé. N’envoyez que le access_token et id_token, jamais le refresh token, et vérifiez la id_token signature et nonce sur votre serveur avant de lui faire confiance, et utilisez un schéma unique à votre application (votre identifiant de bundle inversé).
Le client_secret Un JWT peut durer jusqu'à six mois, mais générer un jeton court par requête évite une panne d'expiration. Conservez le .p8 fichier dans votre secret manager, jamais dans le bundle de l’application.
Étape 5 : Configuration web
Le build web utilise Apple JS avec l'identifiant de service. Ajoutez l'URL exacte de la page qui déclenche la connexion aux URL de retour de l'identifiant de service. Apple n'accepte pas localhost, testez donc la connexion web sur un domaine de préversion déployé ou via un tunnel avec un nom d'hôte réel.
Étape 6 : Initialisation et connexion
Initialize once at app start. Pass the redirectUrl uniquement sur Android et le web. Sur iOS, une redirectUrl fait que le plugin envoie le résultat par POST à cette URL, ce que vous voulez éviter avec le flux natif.
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'
: '',
},
});
}
Déclenchez ensuite la connexion via un bouton. Utilisez un nonce aléatoire pour que votre serveur puisse rejeter les tokens rejoués :
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 réponse vous fournit :
| Champ | Contenu |
|---|---|
result.idToken |
JWT signé par Apple, ce que votre serveur vérifie |
result.profile.user |
L'ID utilisateur stable d'Apple (identique au sub claim) |
result.profile.email |
L'e-mail ou l'adresse de relais privé, si accordé |
result.profile.givenName / familyName |
Uniquement lors de la première autorisation |
result.accessToken |
Avec les paramètres par défaut, cela contient l'autorisation code, et non un vrai token d'accès |
result.authorizationCode |
Présent lors de l'initialisation avec useProperTokenExchange: true |
Si votre serveur a besoin des tokens de rafraîchissement Apple (par exemple pour appeler l’endpoint de révocation lorsqu’un utilisateur supprime son compte), configurez useProperTokenExchange: true dans l' apple configuration et échange authorizationCode sur le serveur.
Étape 7 : Vérifier le token d'identité sur votre backend
Ne faites jamais confiance aux champs du profil envoyés par le client. Vérifiez la signature du JWT avec les clés publiques d’Apple et contrôlez les claims :
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',
};
}
Clé de l’enregistrement utilisateur sur sub, pas sur l’e-mail. L’adresse peut être une adresse relais, et l’utilisateur peut cesser de la partager. Après vérification, émettez votre propre token de session. Si vous utilisez Supabase ou Firebase, ils vérifient le token pour vous, voir Supabase avec Capacitor Social Login.
Règles de l'App Store à garder à l'esprit
- Ligne directrice 4.8: si vous proposez Google, Facebook ou une autre connexion tierce comme système de compte principal, vous devez aussi offrir une option équivalente axée sur la confidentialité. Sign in with Apple répond à cette exigence.
- Directive 5.1.1(v): les applications permettant aux utilisateurs de créer un compte doivent leur permettre de le supprimer depuis l’application. Pour les utilisateurs de Sign in with Apple, appelez l’
https://appleid.apple.com/auth/revokeendpoint lors de la suppression. Consultez notre guide de suppression de compte. - Design du bouton: utilisez les styles de boutons approuvés par Apple et assurez-vous que le bouton est au moins aussi visible que les autres boutons de connexion.
Troubleshooting
| Symptôme | Cause probable | Corriger |
|---|---|---|
ASAuthorizationError code 1000 sur iOS |
Capacité ou entitlement manquant dans la cible compilée | Ajoutez Sign in with Apple dans Xcode, committez App.entitlements, régénérez les profils |
| Code 1001 sur iOS | Utilisateur a annulé | Le plugin rejette avec USER_CANCELLED, à traiter comme une non-erreur |
invalid_client sur Android ou web |
Incorrect clientId (bundle ID utilisé à la place du Services ID) ou incorrect client_secret |
Utilisez l’ID de service, vérifiez l’ID d’équipe, l’ID de clé et le contenu de la clé |
invalid_request: Invalid redirect_uri |
URL de retour non enregistrée à l'identique | Ajoutez l'URL complète https:// à l'ID de service, en respectant les slashs finaux |
| L'app ne revient pas après la connexion Android | Le schéma ou l'hôte de l'intent filter diffère de la redirection du serveur | Comparer com.example.app://apple-login caractère par caractère |
apple.android.redirectUrl is null or empty |
Non redirectUrl sur Android |
Passez votre URL de callback HTTPS lors de l'initialisation sur Android |
Le nom et l'e-mail sont null |
Pas la première autorisation | Lisez-les depuis votre base de données. Révoquez l’app dans les réglages de l’appareil pour retester |
Token rejeté par le backend : aud incohérence |
Le serveur n’accepte qu’une seule audience | Accepter à la fois le bundle ID et le Services ID |
Déployer des correctifs après une release
La plupart des bugs d’authentification se trouvent dans le TypeScript : un client ID incorrect par plateforme, un nonce manquant, un mauvais traitement des erreurs. Ils peuvent être livrés aux utilisateurs le jour même avec les live updates Capgo sans attendre l’App Review. Les modifications des entitlements, du manifest Android ou de MainActivity sont natives et nécessitent un nouveau build store.
Prochaines étapes
- Ajouter Google à côté d’Apple : Comment se connecter avec Google via Capacitor.
- Choisir le bon mélange de méthodes pour votre application : Comment ajouter l'authentification à une application Capacitor.
- Référence du plugin et pages par plateforme : Docs Social Login et le Page de configuration iOS d’Apple.