Langsung ke konten utama

Cara Sign In with Apple Menggunakan Capacitor (2026)

Tambahkan Sign in with Apple ke aplikasi Capacitor 8 di iOS, Android, dan web: setup Apple Developer, plugin code, pengecekan token backend, dan error umum.

Tombol Sign in with Apple berjalan di aplikasi Capacitor dengan plugin Capgo Social Login

Untuk menambahkan Sign in with Apple ke aplikasi Capacitor, install @capgo/capacitor-social-login, aktifkan capability Sign in with Apple di App ID Anda dan di Xcode, lalu panggil SocialLogin.initialize({ apple: {} }), lalu panggil SocialLogin.login({ provider: 'apple' }). Di iOS, ini membuka sheet native Apple dan mengembalikan identity token. Android dan web memakai alur web OAuth Apple, yang butuh Services ID dan HTTPS redirect URL.

Panduan ini membahas setup Apple Developer, client code untuk iOS, Android, dan web, verifikasi token di backend Anda, serta error yang paling sering muncul. Targetnya Capacitor 8 dan Xcode 26, yang diwajibkan App Store Connect untuk upload sejak April 2026.

Yang Anda butuhkan sebelum memulai

Persyaratan Mengapa
Keanggotaan Apple Developer Program berbayar App ID, Services ID, dan key Sign in with Apple hanya tersedia untuk anggota
Mac dengan Xcode 26 Dibutuhkan untuk build dan upload aplikasi iOS di 2026
Proyek Capacitor 8 Plugin v8 menargetkan Capacitor 8
Domain HTTPS yang Anda kontrol Hanya untuk Android dan web. Apple menolak localhost dan URL return HTTP polos
Endpoint backend Selalu verifikasi token. Di Android, ini juga menerima callback Apple

Jika Anda belum memiliki sertifikat penandatanganan, iOS certificate generator kami membuatnya tanpa perlu repot dengan keychain Mac.

Cara kerja Sign in with Apple di tiap platform

Apple hanya menyediakan SDK native untuk platform miliknya sendiri, jadi alurnya berbeda:

Platform Mekanisme Client ID yang digunakan Backend yang diperlukan untuk login
iOS Native ASAuthorizationAppleIDProvider sheet Bundle ID aplikasi Anda Tidak
Android OAuth web Apple di Chrome Custom Tab, Apple mengirim ke server Anda, server mengalihkan kembali ke aplikasi Services ID Ya
Web Popup JS Apple Services ID Tidak (tapi verifikasi token)

Audience (aud) dari identity token mengikuti client ID. Token dari iOS membawa bundle ID Anda, sedangkan token dari Android dan web membawa Services ID. Backend Anda harus menerima keduanya.

Langkah 1: Konfigurasi akun Apple Developer

Aktifkan capability pada App ID Anda

  1. Buka Sertifikat, Identifiers & Profiles dan pilih Pengenal.
  2. Buka App ID yang sesuai dengan Capacitor Anda appId (contohnya com.example.app). Create it if it does not exist.
  3. Di bawah Kemampuan, centang Masuk dengan Apple dan simpan. Biarkan sebagai App ID utama kecuali Anda mengelompokkan beberapa aplikasi.

Jika Anda mengubah capabilities pada App ID yang sudah ada, buat ulang provisioning profiles yang menggunakannya. Profil lama tidak mencakup entitlement baru.

Buat Services ID (hanya Android dan web)

  1. Di Pengenal, klik + lalu pilih Services IDs.
  2. Gunakan deskripsi seperti “Example Web Login” dan identifier seperti com.example.app.signin. Nilai ini menjadi clientId di Android dan web.
  3. Buka Services ID baru, centang Masuk dengan Apple, dan klik Konfigurasi.
  4. Pilih App ID Anda sebagai App ID utama.
  5. Tambahkan domain Anda di bawah Domain dan Subdomain (tanpa scheme, misalnya auth.example.com).
  6. Tambahkan return URL di bawah URL Return dengan lengkap https:// path, misalnya https://auth.example.com/apple/callback. Untuk login web, tambahkan URL halaman yang memanggil login, dengan dan tanpa slash di akhir.

Buat key Sign in with Apple (Android dan pertukaran token server)

  1. Buka Kunci, klik +, centang Masuk dengan Apple, dan klik Konfigurasi untuk memilih App ID utama.
  2. Daftarkan kunci dan unduh .p8 file. Apple hanya mengizinkan Anda mengunduhnya sekali.
  3. Catat Key ID dan ID Tim (ditampilkan di pojok kanan atas developer portal dan di bawah Membership).

Server Anda menggunakan key untuk menandatangani client_secret JWT yang dikirimkannya ke token endpoint Apple.

Daftarkan sumber email jika Anda mengirim email

Pengguna dapat memilih Sembunyikan Email Saya, yang memberi Anda @privaterelay.appleid.com alamat. Apple hanya meneruskan email dari domain dan alamat yang Anda daftarkan di Layanan > Sign in with Apple untuk Komunikasi Email. Lewati ini dan email reset password serta tanda terima akan hilang begitu saja.

Langkah 2: Instal plugin

bun add @capgo/capacitor-social-login
bunx cap sync

Plugin ini menyertakan beberapa provider. Jika Anda hanya memakai Apple (dan mungkin Google), matikan yang lain di capacitor.config.ts agar SDK-nya tidak ikut disertakan:

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;

Jalankan bunx cap sync lagi setelah mengubah blok ini. Provider yang dinonaktifkan tidak tersedia saat runtime, jadi jangan matikan provider yang Anda panggil. Menghapus Facebook juga menghapus AD_ID peringatan izin yang dipicu oleh SDK di Google Play Console.

Langkah 3: Setup iOS

  1. Jalankan bunx cap open ios.
  2. Pilih Aplikasi target, buka Penandatanganan & Kemampuan, klik + Kapabilitas, dan tambahkan Masuk dengan Apple.
  3. Pastikan bundle identifier sesuai dengan App ID yang Anda konfigurasikan.

Xcode menulis com.apple.developer.applesignin ke dalam App.entitlements. Jika Anda build di CI atau dengan Build Capgo, commit file entitlements tersebut, jika tidak cloud build akan menghasilkan app yang gagal dengan ASAuthorizationError code 1000.

Tidak Info.plist URL scheme atau AppDelegate perubahan diperlukan untuk Apple di iOS.

Langkah 4: Setup Android

Apple tidak punya SDK untuk Android. Plugin ini membuka halaman otorisasi Apple di Custom Tab dengan Services ID Anda. Apple lalu mengirim hasilnya sebagai form POST ke URL return HTTPS Anda. Server Anda menukar code, lalu redirect ke deep link yang membuka ulang aplikasi dengan token-nya.

Di android/app/src/main/AndroidManifest.xml, di dalam MainActivity <activity> elemen:

<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>

Teruskan intent ke plugin

Edit MainActivity.java agar deep link sampai ke 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);
    }
}

Filter pada host menjaga deep link lainnya (reset password, link marketing) tetap berfungsi.

Endpoint callback

Apple mengirimkan code, id_token, state, dan pada login pertama user string JSON ke return URL Anda. Endpoint tersebut harus menukarkan code dan mengalihkan ke aplikasi dengan success=true beserta tokennya, yang merupakan kontrak yang diparsing oleh plugin. Contoh minimal Hono menggunakan 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;

Plugin ini membaca token dari query string deep link, jadi apa pun yang Anda masukkan di sana bisa masuk ke logs atau dibaca oleh aplikasi lain yang mendaftarkan custom scheme yang sama. Kirimkan hanya token berdurasi pendek access_token dan id_token, jangan pernah kirim refresh token, verifikasi id_token signature dan nonce di server Anda sebelum memercayainya, dan gunakan scheme yang unik untuk aplikasi Anda (reverse bundle ID Anda).

Panduan client_secret JWT bisa berlaku hingga enam bulan, tetapi membuat JWT berumur pendek untuk setiap request menghindari outage akibat kedaluwarsa. Simpan .p8 file di secret manager Anda, jangan pernah di dalam app bundle.

Langkah 5: Setup web

Build web menggunakan Apple JS dengan Services ID. Tambahkan URL halaman persis yang memicu login ke Return URLs di Services ID. Apple tidak menerima localhost, jadi uji login web pada domain preview yang sudah dideploy atau melalui tunnel dengan hostname asli.

Langkah 6: Inisialisasi dan login

Inisialisasi sekali saat aplikasi dimulai. Teruskan redirectUrl hanya di Android dan web. Di iOS, nilai yang tidak kosong redirectUrl membuat plugin melakukan POST hasil ke URL tersebut, yang tidak Anda inginkan pada alur native.

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'
            : '',
    },
  });
}

Lalu picu login dari tombol. Gunakan nonce acak agar server Anda dapat menolak token yang di-replay:

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;
  }
}

Respons memberikan Anda:

Kolom Konten
result.idToken JWT yang ditandatangani dari Apple, yang diverifikasi oleh server Anda
result.profile.user ID pengguna stabil Apple (sama dengan sub claim)
result.profile.email Email atau alamat relay privat, jika diizinkan
result.profile.givenName / familyName Hanya pada otorisasi pertama
result.accessToken Dengan pengaturan default, ini berisi otorisasi code, bukan access token asli
result.authorizationCode Tersedia saat Anda menginisialisasi dengan useProperTokenExchange: true

Jika server Anda membutuhkan refresh token Apple (misalnya untuk memanggil endpoint revoke saat pengguna menghapus akunnya), set useProperTokenExchange: true di dalam apple konfigurasi dan pertukaran authorizationCode di server.

Langkah 7: Verifikasi token identitas di backend Anda

Jangan pernah percaya pada field profil yang dikirim klien. Verifikasi tanda tangan JWT terhadap kunci publik Apple dan periksa klaimnya:

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',
  };
}

Gunakan sebagai kunci untuk record pengguna sub, bukan email. Email-nya bisa berupa alamat relay, dan pengguna dapat berhenti membagikannya. Setelah verifikasi, terbitkan session token Anda sendiri. Jika Anda menggunakan Supabase atau Firebase, mereka memverifikasi token untuk Anda, lihat Supabase dengan Capacitor Social Login.

Aturan App Store yang perlu diperhatikan

  • Pedoman 4.8: jika Anda menawarkan Google, Facebook, atau login pihak ketiga lainnya sebagai sistem akun utama, Anda juga harus menawarkan opsi setara yang berfokus pada privasi. Sign in with Apple memenuhi syarat.
  • Pedoman 5.1.1(v): aplikasi yang memungkinkan pengguna membuat akun harus memungkinkan mereka menghapus akunnya di dalam aplikasi. Untuk pengguna Sign in with Apple, panggil https://appleid.apple.com/auth/revoke endpoint Apple saat penghapusan. Lihat panduan hapus akun.
  • Desain tombol: gunakan gaya tombol yang disetujui Apple dan pastikan tombolnya setidaknya sama menonjol dengan tombol login lainnya.

Pemecahan Masalah

Gejala Kemungkinan penyebab Perbaikan
ASAuthorizationError code 1000 di iOS Capability atau entitlement hilang di target build Tambahkan Sign in with Apple di Xcode, commit App.entitlements, regenerasi profile
Code 1001 di iOS Dibatalkan oleh user Plugin menolak dengan USER_CANCELLED, anggap bukan error
invalid_client di Android atau web Salah clientId (bundle ID dipakai alih-alih Services ID) atau salah client_secret Gunakan Services ID, cek Team ID, Key ID, dan isi key
invalid_request: Invalid redirect_uri Return URL tidak terdaftar sama persis Tambahkan https:// URL lengkap ke Services ID, cocokkan trailing slash
Aplikasi tidak kembali setelah login Android Scheme atau host Intent filter berbeda dari redirect server Bandingkan com.example.app://apple-login karakter per karakter
apple.android.redirectUrl is null or empty Tidak redirectUrl di Android Lewatkan URL callback HTTPS saat menginisialisasi di Android
Nama dan email adalah null Bukan otorisasi pertama Baca dari database Anda. Cabut aplikasi di pengaturan perangkat untuk menguji lagi
Token ditolak oleh backend: aud mismatch Server hanya menerima satu audience Menerima bundle ID dan Services ID

Mengirim perbaikan setelah rilis

Sebagian besar bug auth ada di TypeScript: client ID yang salah per platform, nonce yang hilang, penanganan error yang buruk. Itu semua bisa dikirim ke pengguna di hari yang sama dengan Live updates Capgo tanpa menunggu App Review. Perubahan pada entitlements, manifest Android, atau MainActivity bersifat native dan membutuhkan build store baru.

Ke mana selanjutnya

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.