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
- Buka Sertifikat, Identifiers & Profiles dan pilih Pengenal.
- Buka App ID yang sesuai dengan Capacitor Anda
appId(contohnyacom.example.app). Create it if it does not exist. - 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)
- Di Pengenal, klik + lalu pilih Services IDs.
- Gunakan deskripsi seperti “Example Web Login” dan identifier seperti
com.example.app.signin. Nilai ini menjadiclientIddi Android dan web. - Buka Services ID baru, centang Masuk dengan Apple, dan klik Konfigurasi.
- Pilih App ID Anda sebagai App ID utama.
- Tambahkan domain Anda di bawah Domain dan Subdomain (tanpa scheme, misalnya
auth.example.com). - Tambahkan return URL di bawah URL Return dengan lengkap
https://path, misalnyahttps://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)
- Buka Kunci, klik +, centang Masuk dengan Apple, dan klik Konfigurasi untuk memilih App ID utama.
- Daftarkan kunci dan unduh
.p8file. Apple hanya mengizinkan Anda mengunduhnya sekali. - 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
- Jalankan
bunx cap open ios. - Pilih Aplikasi target, buka Penandatanganan & Kemampuan, klik + Kapabilitas, dan tambahkan Masuk dengan Apple.
- 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.
Tambahkan deep link
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/revokeendpoint 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
- Tambahkan Google di samping Apple: Cara login dengan Google menggunakan Capacitor.
- Pilih kombinasi metode yang tepat untuk aplikasi Anda: Cara menambah autentikasi ke aplikasi Capacitor.
- Referensi plugin dan halaman platform: Dokumentasi Social Login dan Halaman setup Apple iOS.