Login con Apple en la App Móvil — Finnova
Sign in with Apple es el equivalente de Apple a Google Sign-In: permite al usuario autenticarse con su Apple ID. Finnova lo ofrece como método de login en iOS junto a Google y email/contraseña. Esta página define cómo se integra con el esquema de sesiones de doble token de Finnova (ver Control de Sesiones) y sigue el mismo patrón que el Login con Google.
Obligatorio en iOS: Apple exige (App Store Review Guideline 4.8) que cualquier app que ofrezca login social de terceros (como Google) también ofrezca Sign in with Apple. Por tanto, si Finnova incluye "Continuar con Google", debe incluir "Continuar con Apple" para ser aprobada en la App Store.
1. Principio clave
Igual que con Google, Sign in with Apple solo autentica la identidad, no emite las sesiones de Finnova. El flujo es:
- La app obtiene un Identity Token de Apple (JWT firmado por Apple) usando el SDK nativo.
- La app envía ese token al backend de Finnova.
- El backend verifica el token contra Apple, identifica o crea al usuario, y emite sus propios access + refresh tokens.
Las sesiones siguen siendo de Finnova (mismo esquema JWT de 15 min + refresh opaco de 30 días). Apple solo prueba "este usuario es quien dice ser".
2. Particularidades de Apple frente a Google
| Aspecto | Apple | |
|---|---|---|
| Nombre y email | Siempre disponibles en cada login | Solo se entregan en el primer login — hay que persistirlos en ese momento |
| Ocultar email | No | El usuario puede usar Hide My Email → recibe un alias @privaterelay.appleid.com |
| Identificador estable | sub | sub (el campo sub del Identity Token) |
| Plataformas | iOS, Android, Web | iOS nativo; Android/Web vía flujo web OAuth |
Crítico: Apple envía
fullNamey
3. Configuración previa en Apple Developer
En Apple Developer → Certificates, Identifiers & Profiles:
| Paso | Detalle |
|---|---|
| App ID | Habilitar la capability Sign in with Apple en el App ID de Finnova |
| Service ID | Necesario solo si se soporta el flujo web/Android (identifica al cliente OAuth) |
| Key | Crear una Sign in with Apple private key (.p8) para firmar/validar en el backend si se usa el flujo web |
| Bundle ID | El backend lo valida como audience del Identity Token |
Para login nativo en iOS basta con habilitar la capability en el App ID; el
audiencea validar es el Bundle ID de la app.
4. Librería recomendada
| Stack | Librería |
|---|---|
| Expo | expo-apple-authentication |
| React Native (bare) | @invertase/react-native-apple-authentication |
Sign in with Apple solo está disponible en dispositivos iOS 13+. En Android, el botón debe ocultarse o usar el flujo web (Service ID). El componente nativo
AppleAuthenticationButtoncumple los lineamientos visuales que Apple exige.
5. Flujo de autenticación
6. Verificación del Identity Token en el backend
El backend valida el JWT de Apple contra las claves públicas publicadas en https://appleid.apple.com/auth/keys:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const APPLE_JWKS = createRemoteJWKSet(
new URL('https://appleid.apple.com/auth/keys')
);
async function verifyAppleIdentityToken(identityToken: string) {
const { payload } = await jwtVerify(identityToken, APPLE_JWKS, {
issuer: 'https://appleid.apple.com',
audience: process.env.APPLE_BUNDLE_ID, // Bundle ID de la app
});
if (!payload.sub) throw new Error('Token de Apple inválido');
return {
appleSub: payload.sub as string, // ID estable y único del usuario en Apple
email: payload.email as string | undefined,
emailVerified: payload.email_verified === 'true' || payload.email_verified === true,
isPrivateEmail: payload.is_private_email === 'true', // alias de Hide My Email
};
}
La verificación comprueba la firma, el issuer (appleid.apple.com), el audience (nuestro Bundle ID) y la expiración.
Identificar al usuario por
sub, no por email — sobre todo aquí, porque el email puede ser un alias de Hide My Email que el usuario puede desactivar.
7. Vinculación de cuentas (account linking)
Mismo criterio que Google, con un matiz por Hide My Email:
| Caso | Acción |
|---|---|
apple_sub ya existe | Login directo a esa cuenta |
apple_sub nuevo, pero el email real ya existe en una cuenta | Vincular: añadir apple_sub a la cuenta existente |
Email es alias @privaterelay.appleid.com | Tratar como cuenta nueva (no se puede asumir que coincide con otra) |
apple_sub y email ambos nuevos | Crear cuenta nueva (sin contraseña) |
Cambios al modelo de datos
ALTER TABLE users
ADD COLUMN apple_sub TEXT UNIQUE; -- NULL si nunca usó Apple
CREATE INDEX idx_users_apple_sub ON users(apple_sub);
Esto se suma a
google_subya definido en el Login con Google. Un usuario puede tener ambos vinculados a la misma cuenta.
8. A partir de aquí: sesiones de Finnova
Una vez verificado el usuario, el flujo es idéntico al de email/contraseña y al de Google (ver Control de Sesiones):
- Se crea una fila en
sessionscon la info del dispositivo. - Se emite el par access token (JWT 15 min) + refresh token (opaco 30 días).
- El refresh token se guarda en
SecureStore; el access token en memoria. - Aplican los mismos límites de sesiones por plan, revocación y detección de anomalías.
El
POST /auth/applereemplaza alPOST /auth/loginsolo en la etapa de verificación de identidad. Todo lo demás no cambia.
9. Consideraciones de seguridad
- Verificar siempre el token en el backend contra las claves públicas de Apple; nunca confiar en datos del cliente sin validar.
- Validar el
audience(Bundle ID) para rechazar tokens emitidos para otra app. - Persistir nombre y email en el primer login — Apple no los reenvía después.
- No tratar el alias de Hide My Email como identificador — usar siempre
sub. - No almacenar el Identity Token — se usa una vez y se descarta; lo que persiste son los tokens de Finnova.
- Relay de email: si Finnova envía correos a un alias
@privaterelay.appleid.com, debe configurar el dominio de envío en Apple Developer para que el relay los entregue.
10. Variables de entorno
APPLE_BUNDLE_ID=com.lyratech.finnova
# Solo si se soporta flujo web/Android:
APPLE_SERVICE_ID=com.lyratech.finnova.web
APPLE_TEAM_ID=XXXXXXXXXX
APPLE_KEY_ID=XXXXXXXXXX
APPLE_PRIVATE_KEY=<contenido del .p8>
Para login nativo en iOS basta con APPLE_BUNDLE_ID. Las demás variables solo aplican al flujo web/Android.