Saltar al contenido principal
Tutorial iOS Capacitor

Cómo usar Sign in with Apple con Capacitor (2026)

Añade Sign in with Apple a una app Capacitor 8 en iOS, Android y web: configuración en Apple Developer, plugin code, validación de tokens en el backend y errores comunes.

Cómo usar Sign in with Apple con Capacitor (2026)

Para añadir Sign in with Apple a una app de Capacitor, instala @capgo/capacitor-social-login, habilita la capacidad de Sign in with Apple en tu App ID y en Xcode, y llama a SocialLogin.initialize({ apple: {} }), luego llama a SocialLogin.login({ provider: 'apple' }). En iOS, esto abre la hoja nativa de Apple y devuelve un token de identidad. Android y web usan el flujo OAuth web de Apple, que requiere un Services ID y una URL de redirección HTTPS.

Esta guía cubre la configuración de Apple Developer, el cliente code para iOS, Android y web, la verificación del token en tu backend y los errores más habituales. Está orientada a Capacitor 8 y Xcode 26, que App Store Connect exige para los uploads desde abril de 2026.

Lo que necesitas antes de empezar

Requisito Por qué
Membresía de pago del Apple Developer Program Los App IDs, Services IDs y las claves de Sign in with Apple solo están disponibles para miembros
Un Mac con Xcode 26 Necesario para el build y el upload de apps iOS en 2026
Proyecto Capacitor 8 El plugin v8 es para Capacitor 8
Un dominio HTTPS bajo tu control Solo para Android y web. Apple lo rechaza localhost y URLs de retorno en HTTP simple
Un endpoint de backend Siempre para verificar tokens. En Android también recibe el callback de Apple

Si aún no tienes certificados de firma, el generador de certificados iOS los crea sin complicaciones de Mac keychain.

Cómo funciona Sign in with Apple en cada plataforma

Apple solo ofrece un SDK nativo para sus plataformas, así que el flujo varía:

Plataforma Mecanismo Client ID utilizado Backend necesario para el login
iOS Nativo ASAuthorizationAppleIDProvider Hoja Bundle ID de tu app No
Android OAuth web de Apple en una Chrome Custom Tab, Apple hace POST a tu servidor y el servidor redirige de vuelta a la app ID de servicios Sí
Web Popup de JS de Apple ID de servicios No (pero verifica el token)

El audience (aud) del identity token sigue al client ID. Los tokens de iOS incluyen tu bundle ID, mientras que los de Android y web incluyen el Services ID. Tu backend debe aceptar ambos.

Paso 1: Configura la cuenta de Apple Developer

Habilita la capability en tu App ID

  1. Abrir Certificates, Identifiers & Profiles y selecciona Identificadores.
  2. Abre el App ID que coincida con tu Capacitor appId (por ejemplo com.example.app). Create it if it does not exist.
  3. En Capacidades, marca Iniciar sesión con Apple y guarda. Déjalo como App ID principal a menos que agrupes varias apps.

Si cambias las capabilities de un App ID existente, regenera los provisioning profiles que lo usen. Los perfiles antiguos no incluirán el nuevo entitlement.

Crea un Services ID (solo para Android y web)

  1. En Identificadores, haz clic en + y elige IDs de Servicios.
  2. Usa una descripción como “Example Web Login” y un identificador como com.example.app.signin. Este valor será el clientId en Android y web.
  3. Abre el nuevo Services ID y marca Iniciar sesión con Apple, y haz clic en Configurar.
  4. Selecciona tu App ID como el App ID principal.
  5. Añade tu dominio en Dominios y Subdominios (sin esquema, por ejemplo auth.example.com).
  6. Añade las URLs de retorno en URLs de retorno con el completo https:// ruta completa, por ejemplo https://auth.example.com/apple/callback. Para logins web, añade la URL de la página que llama al login, con y sin la barra final.

Crea una clave de Sign in with Apple (intercambio de tokens para Android y servidor)

  1. Ve a Claves, haz clic en +, marca Iniciar sesión con Apple, y haz clic en Configurar para elegir el App ID principal.
  2. Registra la clave y descarga el .p8 archivo. Apple solo permite descargarlo una vez.
  3. Anota el ID de clave y tu ID de equipo (que aparece en la parte superior derecha del portal de desarrolladores y en Membership).

Tu servidor usa la clave para firmar el client_secret JWT que envía al endpoint de tokens de Apple.

Registra fuentes de email si envías correos

Los usuarios pueden elegir Ocultar mi correo, lo que te proporciona una @privaterelay.appleid.com dirección. Apple solo reenviará correos de dominios y direcciones que registres en Servicios > Sign in with Apple para comunicación por email. Omítelo y tus correos de restablecimiento de contraseña y de recibos desaparecerán sin dejar rastro.

Paso 2: Instalar el plugin

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

El plugin incluye varios providers. Si solo usas Apple (y quizás Google), desactiva los demás en capacitor.config.ts así que sus SDKs no se incluyen:

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;

Ejecuta bunx cap sync de nuevo tras cambiar este bloque. Un provider desactivado no estará disponible en runtime, así que no desactives uno que uses. Eliminar Facebook también quita el AD_ID advertencias de permisos que SDK activa en Google Play Console.

Paso 3: Configuración de iOS

  1. Ejecuta bunx cap open ios.
  2. Selecciona el app objetivo, abrir Signing & Capabilities, haz clic en + Capability, y añade Iniciar sesión con Apple.
  3. Asegúrate de que el bundle identifier coincida con el App ID que configuraste.

Xcode escribe com.apple.developer.applesignin en App.entitlements. Si haces el build en CI o con Build de Capgo, haz commit de ese archivo de entitlements; de lo contrario, el cloud build generará una app que fallará con ASAuthorizationError code 1000.

No Info.plist URL scheme o AppDelegate cambio es necesario para Apple en iOS.

Paso 4: Configuración de Android

Apple no tiene SDK en Android. El plugin abre la página de autorización de Apple en un Custom Tab con tu Services ID. Apple envía el resultado como un form POST a tu URL de retorno HTTPS. Tu servidor intercambia el code, y luego redirige a un deep link que vuelve a abrir la app con los tokens.

En android/app/src/main/AndroidManifest.xml, dentro del MainActivity <activity> elemento:

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

Reenvía el intent al plugin

Editar MainActivity.java para que el deep link llegue al 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);
    }
}

Filtrar por el host permite que tus otros deep links (reset de contraseña, links de marketing) sigan funcionando.

El endpoint de callback

Apple envía code, id_token, state, y en el primer login un user string JSON a tu URL de retorno. El endpoint debe intercambiar el code y redirigir a la app con success=true además de los tokens, que es el contrato que parsea el plugin. Un ejemplo mínimo con Hono usando 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;

El plugin lee los tokens desde el query string del deep link, por lo que cualquier dato ahí podría terminar en los logs o ser leído por otra app que registre el mismo custom scheme. Envía solo el token de corta duración access_token y id_token, nunca el refresh token, verifica la id_token signature and nonce en tu servidor antes de confiar en él, y usa un scheme único para tu app (tu reverse bundle ID).

El client_secret Un JWT puede durar hasta seis meses, pero generar uno corto por cada request evita caídas por expiración. Mantén el .p8 archivo en tu secret manager, nunca en el app bundle.

Paso 5: Configuración web

El build web usa Apple JS con el Services ID. Añade la URL exacta de la página que dispara el login a las return URLs del Services ID. Apple no acepta localhost, así que prueba el login web en un dominio de preview desplegado o mediante un túnel con un hostname real.

Paso 6: Inicializar e iniciar sesión

Inicializa una vez al arrancar la app. Pasa el redirectUrl solo en Android y web. En iOS, un valor no vacío redirectUrl hace que el plugin haga un POST del resultado a esa URL, algo que no quieres en el flujo nativo.

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

Luego activa el login desde un botón. Usa un nonce aleatorio para que tu servidor pueda rechazar tokens replicados:

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

La respuesta te devuelve:

Campo Contenido
result.idToken El JWT firmado por Apple, que es lo que verifica tu servidor
result.profile.user El ID de usuario estable de Apple (igual que el sub claim)
result.profile.email Email o dirección de relay privada, si se concede
result.profile.givenName / familyName Solo en la primera autorización
result.accessToken Con la configuración por defecto, esto contiene el code de autorización, no un token de acceso real
result.authorizationCode Presente al inicializar con useProperTokenExchange: true

Si tu servidor necesita refresh tokens de Apple (por ejemplo, para llamar al endpoint de revocación cuando un usuario borra su cuenta), configura useProperTokenExchange: true en la apple config y haz el exchange authorizationCode en el servidor.

Paso 7: Verifica el identity token en tu backend

Nunca confíes en los campos del perfil que envía el cliente. Verifica la firma del JWT con las claves públicas de Apple y revisa los 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',
  };
}

Usa como clave del registro de usuario sub, no el email. El email puede ser una dirección de relay y los usuarios pueden dejar de compartirlo. Tras la verificación, emite tu propio token de sesión. Si usas Supabase o Firebase, ellos verifican el token por ti; consulta Supabase con Capacitor Social Login.

Reglas de la App Store a tener en cuenta

  • Directriz 4.8: si ofreces Google, Facebook u otro login de terceros como sistema principal, debes ofrecer una opción equivalente centrada en la privacidad. Sign in with Apple cumple con esto.
  • Directriz 5.1.1(v): las apps que permiten crear cuentas deben permitir borrarlas desde la app. Para usuarios de Sign in with Apple, llama al https://appleid.apple.com/auth/revoke endpoint al eliminarlo. Consulta nuestra guía de eliminación de cuentas.
  • Diseño del botón: usa los estilos de botón aprobados por Apple y asegúrate de que el botón sea al menos tan prominente como los demás botones de login.

Solución de problemas

Síntoma Causa probable Solución
ASAuthorizationError code 1000 en iOS Falta la capability o el entitlement en el target compilado Añade Sign in with Apple en Xcode y haz commit App.entitlements, regenera los profiles
Code 1001 en iOS El usuario canceló El plugin rechaza con USER_CANCELLED, no lo trates como error
invalid_client en Android o web Incorrecto clientId (se usó el bundle ID en vez del Services ID) o incorrecto client_secret Usa el Services ID, revisa el Team ID, Key ID y el contenido de la key
invalid_request: Invalid redirect_uri La URL de retorno no coincide exactamente Añade la URL completa https:// al Services ID; verifica las barras finales
La app no regresa tras el login en Android El scheme o host del Intent filter no coincide con el redirect del servidor Comparar com.example.app://apple-login carácter por carácter
apple.android.redirectUrl is null or empty No redirectUrl en Android Pasa tu URL de callback HTTPS al inicializar en Android
El nombre y el email son null No es la primera autorización Léelos de tu base de datos. Revoca la app en los ajustes del dispositivo para probar de nuevo
Token rechazado por el backend: aud no coincide El servidor solo acepta una audiencia Acepta tanto el bundle ID como el Services ID

Lanzar fixes después del release

La mayoría de los bugs de auth están en TypeScript: un client ID incorrecto por plataforma, un nonce faltante o un mal manejo de errores. Eso se puede enviar a los usuarios el mismo día con actualizaciones Live Update de Capgo sin esperar a la App Review. Los cambios en entitlements, el Android manifest o MainActivity son nativos y requieren un nuevo build para la store.

Próximos pasos

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.