メインコンテンツへスキップ

Capacitor を使用した Sign In with Apple の実装方法 (2026)

Capacitor 8 アプリに iOS、Android、Web 向けに Sign in with Apple を追加: Apple Developer の設定、プラグインの code、バックエンドのトークン検証、よくあるエラー。

Capacitor アプリで Capgo Social Login プラグインを使用して動作する Sign in with Apple ボタン

Capacitor アプリに Sign in with Apple を追加するには、インストールします @capgo/capacitor-social-login、App ID と Xcode で Sign in with Apple の機能を有効にし、呼び出します SocialLogin.initialize({ apple: {} })、その後呼び出す SocialLogin.login({ provider: 'apple' })。iOS ではネイティブの Apple シートが開き、identity token が返されます。Android と Web は Apple の Web OAuth フローを使用し、Services ID と HTTPS リダイレクト URL が必要です。

このガイドでは、Apple Developer のセットアップ、iOS/Android/Web 向けのクライアント code、バックエンドでのトークン検証、および遭遇しやすいエラーについて説明します。対象は Capacitor 8 と Xcode 26 で、2026年4月以降 App Store Connect のアップロードに必須とされています。

開始前に必要なもの

要件 理由
有料の Apple Developer Program 会員資格 App ID、Services ID、Sign in with Apple キーは会員のみ利用可能です
Xcode 26 対応の Mac 2026年にiOSアプリのビルドとアップロードに必要なもの
Capacitor 8 プロジェクト プラグイン v8 は Capacitor 8 に対応
管理できる HTTPS ドメイン Android と Web のみ。Apple は拒否します localhost 平文 HTTP のリターン URL も
バックエンドのエンドポイント 常にトークン検証に使用。Android では Apple のコールバックも受信します

まだ署名証明書を持っていない場合、 iOS証明書ジェネレーター Mac のキーチェーン操作なしで生成します。

各プラットフォームでの Sign in with Apple の仕組み

Appleは自社プラットフォーム向けにしかネイティブのSDKを提供していないため、フローが異なります:

プラットフォーム 仕組み 使用するClient ID ログインに必要なバックエンド
iOSと ネイティブ ASAuthorizationAppleIDProvider シート アプリのBundle ID いいえ
Androidでは Chrome Custom TabでAppleのWeb OAuthを実行し、AppleがサーバーにPOST、サーバーがアプリへリダイレクトします Services ID はい
Web Apple JS ポップアップ Services ID いいえ(ただしトークンは検証してください)

Audience(aud)は client ID に応じて決まります。iOS のトークンには bundle ID が、Android と Web のトークンには Services ID が入ります。バックエンドでは両方を受け入れる必要があります。

ステップ 1: Apple Developer アカウントの設定

App ID の capability を有効にする

  1. 開く Certificates, Identifiers & Profilesで作成します。 を選択し、 識別子.
  2. Capacitor に一致する App ID を開く appId (例: com.example.app)。存在しない場合は作成してください。
  3. 未満 機能を開き、 Appleでサインイン して保存します。複数のアプリをグループ化しない限り、プライマリ App ID のままにしてください。

既存の App ID の capabilities を変更した場合、それを使用している provisioning profiles を再生成してください。古い profile には新しい entitlement が含まれません。

Services ID を作成(Android と Web のみ)

  1. では 識別子、クリックして + を選択します サービスID.
  2. 「Example Web Login」のような説明と、 com.example.app.signinのような識別子を使用します。この値は clientId がAndroidとWebでになります。
  3. 新しいサービスIDを開き、 Appleでサインイン、クリックして 設定.
  4. App ID をプライマリ App ID として選択します。
  5. ドメインを以下に追加します: ドメインとサブドメイン (スキームなし、例: auth.example.com).
  6. 以下にリターンURLを追加: リターンURL 完全な https:// パス、例: https://auth.example.com/apple/callback。Webログインの場合は、末尾スラッシュの有無にかかわらず、ログインを呼び出すページのURLを追加してください。

Sign in with Apple キーを作成(Android とサーバーでのトークン交換用)

  1. 以下に移動します: キー、クリックして +を開き、 Appleでサインイン、クリックして 設定 プライマリ App ID を選択します。
  2. キーを登録し、ファイルをダウンロードしてください .p8 Apple では一度しかダウンロードできません。
  3. 以下を控えてください: Key ID と、あなたの チームID (デベロッパーポータルの右上と Membership 内に表示されます)。

サーバーはこのキーを使って、 client_secret Apple のトークンエンドポイントに送信する JWT に署名します。

メールを送信する場合は、メールソースを登録してください

ユーザーが選択可能 Hide My Email、これにより @privaterelay.appleid.com アドレスが提供されます。Appleは、登録したドメインとアドレスからのメールのみ転送します サービス > メール通信用の Sign in with Apple。これをスキップすると、パスワードリセットや領収書のメールが静かに失われます。

ステップ 2: プラグインのインストール

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

このプラグインは複数のプロバイダーをバンドルしています。Apple(および場合によってはGoogle)のみを使用する場合は、他のプロバイダーを無効にしてください capacitor.config.ts そのため、これらのSDKはバンドルに含まれません:

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;

実行 bunx cap sync このブロックを変更した後に再度実行してください。無効化されたプロバイダーは実行時に利用できないため、呼び出しているプロバイダーを無効にしないでください。Facebookを削除すると、 AD_ID Google Play ConsoleでそのSDKが発生させる権限警告も表示されなくなります。

ステップ3: iOSセットアップ

  1. 実行 bunx cap open ios.
  2. 次の アプリ ターゲットを選択し、 Signing & Capabilities、クリックして + 機能、追加します Appleでサインイン.
  3. バンドル識別子が設定した App ID と一致することを確認してください。

Xcode は com.apple.developer.applesignin 次のように分類しています App.entitlementsを書き出します。CI や Capgo のビルドでビルドする場合は、そのエンタイトルメントファイルをコミットしてください。そうしないとクラウドビルドで生成されたアプリが ASAuthorizationError code 1000 で失敗します。

いいえ Info.plist URL スキームまたは AppDelegate の変更が iOS の Apple に対して必要です。

ステップ4: Android のセットアップ

Apple には Android 用の SDK はありません。プラグインは Services ID を使って Apple の認可ページを Custom Tab で開きます。その後、Apple は結果をフォーム POST として HTTPS のリターン URL に送信します。サーバーは code を交換してから、トークン付きでアプリを再度開くディープリンクにリダイレクトします。

では android/app/src/main/AndroidManifest.xml、次の MainActivity <activity> 要素内に:

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

Intent をプラグインに転送

編集 MainActivity.java ディープリンクがプラグインに届くようにします:

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

ホストでフィルタリングすることで、他のディープリンク(パスワードリセット、マーケティングリンクなど)が引き続き機能します。

コールバックエンドポイント

Apple が POST します code, id_token, state、初回ログイン時に user JSON文字列をリターンURLに渡します。エンドポイントはcodeを交換し、アプリにリダイレクトする必要があります success=true トークンも含まれます。これがプラグインが解析する契約です。最小限のHonoの例として 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;

プラグインはディープリンクのクエリ文字列からトークンを読み取ります。そのため、そこに含めた情報はログに残ったり、同じカスタムスキームを登録した別のアプリに読み取られたりする可能性があります。短期間の access_token と id_tokenのみを送信し、リフレッシュトークンは送らず、 id_token 署名を検証し nonce サーバー側で信頼する前に確認し、アプリ固有のスキーム(リバースドメイン形式のバンドルID)を使用してください。

この client_secret JWTは最大6ヶ月間有効ですが、リクエストごとに短期間のものを作成することで、期限切れによる障害を回避できます。 .p8 ファイルをシークレットマネージャーに保管し、アプリバンドル内には決して含めないでください。

ステップ5: Webセットアップ

Webビルドでは、Services IDとApple JSを使用します。ログインをトリガーする正確なページURLを、Services IDのリターンURLに追加してください。Appleは localhostを受け入れないため、デプロイ済みのプレビュードメインまたは実ホスト名を持つトンネル経由でWebログインをテストしてください。

ステップ 6: 初期化とサインイン

アプリ起動時に一度だけ初期化します。 redirectUrl はAndroidとWebでのみ渡します。iOSでは、空でない redirectUrl を指定すると、プラグインが結果をそのURLにPOSTしますが、ネイティブフローではこれを避けたいです。

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

次に、ボタンからログインをトリガーします。サーバーがリプレイされたトークンを拒否できるように、ランダムなnonceを使用します:

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

レスポンスから以下が得られます:

項目 内容
result.idToken Appleからの署名済みJWT(サーバーで検証するもの)
result.profile.user Appleの安定したユーザーID( sub claim)
result.profile.email 許可された場合のメールアドレスまたはプライベートリレーアドレス
result.profile.givenName / familyName 初回の認可時のみ
result.accessToken デフォルト設定では、これは実際のアクセストークンではなく、認可codeを保持します
result.authorizationCode で初期化すると存在します useProperTokenExchange: true

サーバーがAppleのリフレッシュトークンを必要とする場合(たとえば、ユーザーがアカウントを削除したときにrevokeエンドポイントを呼び出す場合)は、設定してください useProperTokenExchange: true の apple config を設定して交換します authorizationCode サーバー上で。

ステップ7: バックエンドでIDトークンを検証

クライアントが送信するプロファイルフィールドを信頼しないでください。Appleの公開鍵でJWT署名を検証し、クレームを確認してください:

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

ユーザーレコードのキーには sub、メールではありません。メールはリレーアドレスである可能性があり、ユーザーは共有を停止できます。検証後、独自のセッショントークンを発行してください。SupabaseやFirebaseを使用している場合、トークンの検証を代行してくれます。詳細は Supabase と Capacitor Social Login.

念頭に置くべきApp Storeのルール

  • ガイドライン4.8:Google、Facebook、またはその他のサードパーティログインをメインのアカウントシステムとして提供する場合、同等のプライバシー重視のオプションも提供する必要があります。Sign in with Appleはこれに該当します。
  • ガイドライン 5.1.1(v):ユーザーがアカウントを作成できるアプリは、アプリ内でアカウントを削除できるようにする必要があります。Sign in with Appleユーザーの場合、削除時にAppleの https://appleid.apple.com/auth/revoke エンドポイントを呼び出してください。当社の アカウント削除ガイド.
  • ボタンのデザイン:Appleが承認したボタンスタイルを使用し、他のログインボタンと同程度以上に目立たせる必要があります。

トラブルシューティング

症状 考えられる原因 解決策
ASAuthorizationError iOSでのcode 1000 ビルドターゲットにCapabilityまたはentitlementが不足しています XcodeでSign in with Appleを追加してコミットし、 App.entitlementsプロファイルを再生成してください
iOSでのCode 1001 ユーザーがキャンセルしました プラグインは次のエラーでrejectします: USER_CANCELLEDエラーとして扱わないでください
invalid_client AndroidまたはWeb環境で 誤り clientId (Services IDではなくbundle IDが使用されている) または不正な client_secret Services IDを使用し、Team ID、Key ID、およびキーの内容を確認してください
invalid_request: Invalid redirect_uri Return URLが完全一致で登録されていません 完全な https:// URLをServices IDに追加し、末尾のスラッシュまで一致させてください
Androidでログイン後にアプリに戻らない Intent filterのschemeまたはhostがサーバー側のredirectと異なります 比較 com.example.app://apple-login 1文字ずつ
apple.android.redirectUrl is null or empty いいえ redirectUrl Androidの場合 Androidの初期化時にHTTPSコールバックURLを渡してください
名前とメールアドレスは null 初回の認可ではありません データベースから読み取ってください。再テストするには、デバイスの設定でアプリの認可を取り消してください
バックエンドでトークンが拒否されました: aud 不一致 サーバーはaudienceを1つしか受け付けません bundle IDとServices IDの両方を受け入れてください

リリース後の修正を配信

Most auth bugs live in TypeScript: a wrong client ID per platform, a missing nonce, bad error handling. Those can be shipped to users the same day with Capgo Live Updates App Reviewを待つ必要はありません。entitlementsやAndroid manifest、または MainActivity ネイティブ側の変更なので、ストア用の新しいビルドが必要です。

次のステップ

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。