Um Sign in with Apple in einer Capacitor-App zu nutzen, installieren Sie @capgo/capacitor-social-login, aktivieren Sie die Sign in with Apple-Fähigkeit in Ihrer App ID und in Xcode, und rufen Sie auf SocialLogin.initialize({ apple: {} }), dann aufrufen SocialLogin.login({ provider: 'apple' }). Auf iOS öffnet dies das native Apple-Blatt und gibt ein Identity-Token zurück. Android und Web nutzen Apples Web-OAuth-Flow, der eine Services ID und eine HTTPS-Redirect-URL benötigt.
Dieser Guide behandelt die Apple-Developer-Einrichtung, den Client-code für iOS, Android und Web, die Token-Verifizierung auf Ihrem Backend und die häufigsten Fehler. Er richtet sich an Capacitor 8 und Xcode 26, die App Store Connect seit April 2026 für Uploads verlangt.
Was Sie vor dem Start benötigen
| Voraussetzung | Warum |
|---|---|
| Bezahlte Apple Developer Program-Mitgliedschaft | App IDs, Services IDs und Sign in with Apple-Keys sind nur für Mitglieder verfügbar |
| Ein Mac mit Xcode 26 | Erforderlich zum Bauen und Hochladen von iOS-Apps in 2026 |
| Capacitor 8 Projekt | Plugin v8 zielt auf Capacitor 8 ab |
| Eine HTTPS-Domain unter deiner Kontrolle | Nur für Android und Web. Apple lehnt das ab localhost und HTTP-Return-URLs ohne TLS |
| Ein Backend-Endpoint | Immer zur Token-Verifizierung. Auf Android empfängt er auch Apples Callback |
Falls du noch keine Signing-Zertifikate hast, dann iOS-Zertifikatsgenerator erstellt sie ohne das Mac-Keychain-Gefummel.
So funktioniert Sign in with Apple auf jeder Plattform
Apple liefert nur ein natives SDK für die eigenen Plattformen aus, daher unterscheidet sich der Flow:
| Plattform | Mechanismus | Verwendete Client-ID | Backend für Login erforderlich |
|---|---|---|---|
| iOS | Native ASAuthorizationAppleIDProvider Sheet |
Bundle-ID Ihrer App | Nein |
| Android | Apple-Web-OAuth in einem Chrome Custom Tab, Apple postet an Ihren Server, der Server leitet zurück in die App weiter | Services ID | Ja |
| Web | Apple JS-Popup | Services ID | Nein (aber Token verifizieren) |
Das Audience (aud) des Identity Tokens folgt der Client ID. Tokens von iOS tragen deine Bundle ID, Tokens von Android und Web die Services ID. Dein Backend muss beide akzeptieren.
Schritt 1: Apple Developer Account konfigurieren
Capability für deine App ID aktivieren
- Öffne Zertifikate, Identifiers & Profiles und wählen Sie Identifikatoren.
- Öffnen Sie die App ID, die zu Ihrer Capacitor passt
appIdsein (zum Beispielcom.example.app). Erstellen Sie sie, falls sie nicht existiert. - Unter Funktionen, tick Mit Apple anmelden und speichern Sie. Lassen Sie sie als primäre App ID, es sei denn, Sie gruppieren mehrere Apps.
Wenn Sie die Berechtigungen einer vorhandenen App ID ändern, generieren Sie die Provisionierungsprofile neu, die sie verwenden. Alte Profile enthalten die neue Berechtigung nicht.
Erstellen Sie eine Services ID (nur Android und Web)
- Unter Identifier, klicke auf + und wähle Service-IDs.
- Nutze eine Beschreibung wie „Example Web Login“ und einen Identifier wie
com.example.app.signin. Dieser Wert wird zurclientIdauf Android und im Web. - Öffne die neue Services ID, aktiviere Mit Apple anmelden, und klicke auf Konfigurieren.
- Wähle deine App ID als primäre App ID aus.
- Füge deine Domain unter Domains und Subdomains (ohne Scheme, z. B.
auth.example.com). - Füge die Return URLs unter Rückgabe-URLs mit dem vollständigen
https://Pfad hinzu, z. B.https://auth.example.com/apple/callback. Für Web-Logins die Seiten-URL hinzufügen, die den Login aufruft, mit und ohne Slash am Ende.
Schlüssel für Sign in with Apple erstellen (Android und Server-Token-Austausch)
- Gehe zu Schlüssel, klicke auf +, Häkchen Mit Apple anmelden, und klicke auf Konfigurieren um die primäre App ID auszuwählen.
- Registriere den Key und lade die
.p8Datei herunter. Apple lässt nur einen Download zu. - Notiere dir die Key-ID und deine Team-ID (oben rechts im Developer Portal und unter Membership zu finden).
Dein Server nutzt den Key, um das client_secret JWT zu signieren, das er an Apples Token-Endpoint sendet.
E-Mail-Quellen registrieren, falls du E-Mails versendest
Nutzer können wählen Meine E-Mail verbergen, was dir eine @privaterelay.appleid.com Adresse liefert. Apple leitet E-Mails nur von Domains und Adressen weiter, die du unter Services > Sign in with Apple für die E-Mail-Kommunikationregistriert hast. Überspringe dies, und deine Passwort-Reset- und Beleg-E-Mails verschwinden lautlos.
Schritt 2: Plugin installieren
bun add @capgo/capacitor-social-login
bunx cap sync
Das Plugin bündelt mehrere Provider. Wenn du nur Apple (und evtl. Google) nutzt, schalte die anderen in capacitor.config.ts damit ihre SDKs nicht ausgeliefert werden:
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;
Ausführen bunx cap sync erneut nach der Änderung dieses Blocks. Ein deaktivierter Provider ist zur Laufzeit nicht verfügbar, daher nicht deaktivieren, wenn du ihn aufrufst. Das Entfernen von Facebook löscht auch AD_ID Berechtigungswarnungen, die SDK in der Google Play Console auslöst.
Schritt 3: iOS-Setup
- Ausführen
bunx cap open ios. - Wähle die App hinzu Target, öffne Signing & Capabilities, klicke auf + Capability, und füge hinzu Mit Apple anmelden.
- Stelle sicher, dass die Bundle ID mit der konfigurierten App ID übereinstimmt.
Xcode schreibt com.apple.developer.applesignin in App.entitlements. Wenn du in CI oder mit Capgo Build, committet diese Entitlements-Datei, sonst erzeugt der Cloud-Build eine App, die mit ASAuthorizationError code 1000.
Nein Info.plist URL-Schema oder AppDelegate Änderung ist für Apple auf iOS erforderlich.
Schritt 4: Android-Setup
Apple bietet keinen Android-SDK. Das Plugin öffnet Apples Autorisierungsseite in einem Custom Tab mit deiner Services ID. Apple sendet das Ergebnis anschließend als Form-POST an deine HTTPS-Return-URL. Dein Server tauscht den code ein und leitet dann an einen Deep Link weiter, der die App mit den Tokens erneut öffnet.
Deep Link hinzufügen
Unter android/app/src/main/AndroidManifest.xml, innerhalb der MainActivity <activity> Element:
<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>
Leite den Intent an das Plugin weiter
Bearbeiten MainActivity.java damit der Deep Link das Plugin erreicht:
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);
}
}
Wenn du nach Host filterst, funktionieren deine anderen Deep Links (Passwort-Reset, Marketing-Links) weiter.
Der Callback-Endpoint
Apple sendet code, id_token, state, und beim ersten Login einen user JSON-String an deine Return-URL. Der Endpoint muss den code austauschen und die App mit folgendem Inhalt weiterleiten: success=true plus den Tokens, was der Contract ist, den das Plugin parst. Ein minimales Hono-Beispiel mit 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;
Das Plugin liest die Tokens aus der Deep-Link-Query-String, daher kann alles, was du dort einfügst, in Logs landen oder von einer anderen App gelesen werden, die denselben Custom Scheme registriert. Sende nur die kurzlebigen access_token und id_token, niemals den Refresh-Token, verifiziere die id_token Signatur und nonce auf deinem Server, bevor du ihr vertraust, und nutze einen für deine App eindeutigen Scheme (deine umgekehrte Bundle-ID).
Der client_secret JWTs können bis zu sechs Monate gültig sein, aber die Generierung eines kurzen pro Request vermeidet eine Ablauf-Störung. Behalte die .p8 Datei in deinem Secret Manager, niemals im App-Bundle.
Schritt 5: Web-Setup
Der Web-Build verwendet Apple JS mit der Services ID. Füge die exakte Seiten-URL, die den Login auslöst, zu den Return-URLs der Services ID hinzu. Apple akzeptiert keine localhost, teste daher den Web-Login auf einer deployten Preview-Domain oder über einen Tunnel mit einem echten Hostnamen.
Schritt 6: Initialisieren und anmelden
Initialisiere einmalig beim App-Start. Übergebe die redirectUrl nur auf Android und Web. Auf iOS führt ein nicht leerer redirectUrl dazu, dass das Plugin das Ergebnis an diese URL POSTet – das ist im nativen Flow nicht gewünscht.
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'
: '',
},
});
}
Löse den Login anschließend über einen Button aus. Verwende einen zufälligen Nonce, damit dein Server replizierte Tokens ablehnen kann:
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;
}
}
Die Antwort liefert dir:
| Feld | Inhalt |
|---|---|
result.idToken |
Signiertes JWT von Apple, das dein Server verifiziert |
result.profile.user |
Stabile Benutzer-ID von Apple (identisch mit der sub claim) |
result.profile.email |
E-Mail-Adresse oder Private-Relay-Adresse, wenn gewährt |
result.profile.givenName / familyName |
Nur bei der ersten Autorisierung |
result.accessToken |
Mit den Standardeinstellungen enthält dies den Authorization-code, keinen echten Access Token |
result.authorizationCode |
Vorhanden bei der Initialisierung mit useProperTokenExchange: true |
Falls dein Server Apple Refresh Tokens benötigt (z. B. um den Revoke-Endpoint aufzurufen, wenn ein Nutzer sein Konto löscht), setze useProperTokenExchange: true im apple config und tausche authorizationCode auf dem Server aus.
Schritt 7: Den Identity Token im Backend verifizieren
Vertraue niemals den Profilfeldern, die der Client sendet. Verifiziere die JWT-Signatur gegen Apples Public Keys und prüfe die 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',
};
}
Nutze als Schlüssel für den User-Record sub, nicht die E-Mail. Die E-Mail kann eine Relay-Adresse sein, und Nutzer können das Teilen wieder deaktivieren. Nach der Verifizierung gib deinen eigenen Session Token aus. Wenn du Supabase oder Firebase nutzt, verifizieren die den Token für dich, siehe Supabase mit Capacitor Social Login.
App-Store-Regeln, die du beachten solltest
- Richtlinie 4.8: Wenn Sie Google, Facebook oder einen anderen Drittanbieter-Login als Haupt-Anmeldesystem anbieten, müssen Sie auch eine gleichwertige datenschutzfreundliche Option anbieten. Sign in with Apple erfüllt das.
- Richtlinie 5.1.1(v)Apps, die die Erstellung von Benutzerkonten ermöglichen, müssen auch deren Löschung innerhalb der App erlauben. Für Nutzer von Sign in with Apple muss Apples
https://appleid.apple.com/auth/revokeEndpoint auf. Siehe unseren Leitfaden zur Kontolöschung. - Button-Design: Verwenden Sie die von Apple freigegebenen Button-Styles und machen Sie den Button mindestens genauso prominent wie andere Login-Buttons.
Fehlerbehebung
| Symptom | Wahrscheinliche Ursache | Lösung |
|---|---|---|
ASAuthorizationError code 1000 auf iOS |
Fehlende Berechtigung oder Entitlement im gebauten Ziel | Sign in with Apple in Xcode hinzufügen, committen App.entitlements, Profile neu generieren |
| Code 1001 auf iOS | Benutzer hat abgebrochen | Das Plugin lehnt mit ab USER_CANCELLED, als Nicht-Fehler behandeln |
invalid_client auf Android oder Web |
Falsch clientId (Bundle-ID wird anstelle der Services-ID verwendet) oder ungültig client_secret |
Services-ID verwenden, Team-ID, Key-ID und Schlüsselinhalt prüfen |
invalid_request: Invalid redirect_uri |
Return-URL nicht exakt registriert | Die vollständige https:// URL zur Services-ID hinzufügen, schließende Schrägstriche abgleichen |
| App kehrt nach Android-Login nicht zurück | Intent-Filter-Schema oder Host weicht vom Server-Redirect ab | Vergleichen com.example.app://apple-login Zeichen für Zeichen |
apple.android.redirectUrl is null or empty |
Nein redirectUrl auf Android |
HTTPS-Callback-URL beim Initialisieren auf Android übergeben |
Name und E-Mail sind null |
Nicht die erste Autorisierung | Lies sie aus deiner Datenbank. App in den Geräteeinstellungen widerrufen, um erneut zu testen |
Token vom Backend abgelehnt: aud mismatch |
Server akzeptiert nur ein Audience | Sowohl Bundle ID als auch Services ID akzeptieren |
Fixes nach dem Release ausliefern
Die meisten Auth-Bugs stecken im TypeScript: falsche Client-ID pro Plattform, fehlendes Nonce, schlechte Fehlerbehandlung. Das kann man am selben Tag mit Capgo Live Updates ausliefern, ohne auf App Review zu warten. Änderungen an Entitlements, dem Android-Manifest oder MainActivity sind nativ und erfordern einen neuen Store-Build.
Wie es weitergeht
- Google neben Apple hinzufügen: So meldest du dich mit Google über Capacitor an.
- Wähle die richtige Kombination aus Methoden für deine App: So fügen Sie einer Capacitor-App Authentifizierung hinzu.
- Plugin-Referenz und Plattformseiten: Social Login Docs und Apple iOS Setup-Seite.