Zum Hauptinhalt springen

Sign in with Apple mit Capacitor (2026)

Füge Sign in with Apple zu einer Capacitor 8-App auf iOS, Android und Web hinzu: Apple-Developer-Setup, Plugin-code, Backend-Token-Prüfungen und häufige Fehler.

Apple-Sign-in-Button in einer Capacitor-App mit dem Capgo Social Login-Plugin

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

  1. Öffne Zertifikate, Identifiers & Profiles und wählen Sie Identifikatoren.
  2. Öffnen Sie die App ID, die zu Ihrer Capacitor passt appId sein (zum Beispiel com.example.app). Erstellen Sie sie, falls sie nicht existiert.
  3. 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)

  1. Unter Identifier, klicke auf + und wähle Service-IDs.
  2. Nutze eine Beschreibung wie „Example Web Login“ und einen Identifier wie com.example.app.signin. Dieser Wert wird zur clientId auf Android und im Web.
  3. Öffne die neue Services ID, aktiviere Mit Apple anmelden, und klicke auf Konfigurieren.
  4. Wähle deine App ID als primäre App ID aus.
  5. Füge deine Domain unter Domains und Subdomains (ohne Scheme, z. B. auth.example.com).
  6. 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)

  1. Gehe zu Schlüssel, klicke auf +, Häkchen Mit Apple anmelden, und klicke auf Konfigurieren um die primäre App ID auszuwählen.
  2. Registriere den Key und lade die .p8 Datei herunter. Apple lässt nur einen Download zu.
  3. 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

  1. Ausführen bunx cap open ios.
  2. Wähle die App hinzu Target, öffne Signing & Capabilities, klicke auf + Capability, und füge hinzu Mit Apple anmelden.
  3. 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.

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

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.