Global Sign Out con AWS Cognito en iOS

En los artículos anteriores implementamos autenticación, recuperación de contraseña y cambio de contraseña. También tenemos un botón de “Cerrar Sesión” en HomeView que elimina los tokens del Keychain local. Pero, ¿qué pasa si un usuario quiere cerrar sesión en TODOS sus dispositivos al mismo tiempo? En este artículo implementaremos el Global Sign Out.

¿Qué vamos a construir?

En este artículo implementaremos:

  1. Método globalSignOut – Invalidar tokens en todos los dispositivos
  2. Actualizar HomeView – Ofrecer dos opciones: Sign Out Local vs Global
  3. Diferenciar ambos flujos – Local (rápido) vs Global (seguro)
  4. Manejo de errores – Token inválido, sesión expirada
  5. Testing – Probar cierre de sesión en múltiples escenarios

Diferencia: Local Sign Out vs Global Sign Out

Local Sign Out (Artículo 3 – actual):

❌ NO llama a Cognito API

✅ Solo elimina tokens del Keychain en ESTE dispositivo

⚠️ Tokens siguen válidos en otros dispositivos

⚡ Rápido (no requiere red)

📱 Usuario puede seguir logueado en otros dispositivos

Global Sign Out :

✅ Llama a Cognito API (globalSignOut())

✅ Invalida tokens en TODOS los dispositivos del usuario

✅ Tokens dejan de funcionar inmediatamente en todas partes

🌐 Requiere conexión a internet

🔒 Más seguro (cierre de sesión completo)

El flujo de Global Sign Out

Usuario en HomeView
    ↓
Presiona «Cerrar Sesión Global»
    ↓
Alert: «¿Cerrar sesión en TODOS los dispositivos?»
    ↓
Confirma
    ↓
globalSignOut(accessToken)
    ↓
Cognito invalida tokens en servidor
    ↓
Eliminar tokens del Keychain local
    ↓
✅ Sesión cerrada GLOBALMENTE
    ↓
Usuario debe iniciar sesión nuevamente en CUALQUIER dispositivo

Casos de uso

¿Cuándo usar Local Sign Out? – Usuario solo quiere cerrar sesión en este dispositivo – Usuario sigue usando la app en otros dispositivos – No hay preocupación de seguridad

¿Cuándo usar Global Sign Out? – Usuario sospecha que su cuenta fue comprometida – Usuario perdió un dispositivo con la sesión activa – Usuario quiere forzar re-autenticación en todos lados – Políticas de seguridad empresarial (ej: empleado despedido) – Usuario cambió su contraseña y quiere invalidar sesiones antiguas

Requisitos previos

  • Haber completado los artículos anteriores (especialmente Artículo 3: Login)
  • Tener CognitoAuthService implementado
  • Tener HomeView con botón de “Cerrar Sesión”
  • Usuario logueado con tokens en Keychain

Paso 1: Agregar método globalSignOut en CognitoAuthService

Primero agregamos el método para cerrar sesión globalmente en CognitoAuthService.swift:

// Global Sign Out

/// Cierra la sesión del usuario en TODOS los dispositivos
/// Invalida los tokens en el servidor de Cognito y elimina los tokens locales
/// - Returns: Mensaje de confirmación
func globalSignOut() async throws -> String {
    guard let client = cognitoClient else {
        throw NSError(domain: "CognitoAuthService", code: -1,
                     userInfo: [NSLocalizedDescriptionKey: "Cliente de Cognito no inicializado"])
    }

    // Obtener Access Token del usuario logueado
    guard let accessToken = KeychainManager.shared.readTokens()?.accessToken else {
        throw NSError(domain: "CognitoAuthService", code: 9001,
                     userInfo: [NSLocalizedDescriptionKey: "No hay sesión activa. Por favor inicia sesión"])
    }

    // Crear la solicitud de global sign out
    let globalSignOutInput = GlobalSignOutInput(
        accessToken: accessToken
    )

    do {
        _ = try await client.globalSignOut(input: globalSignOutInput)

        print("✅ Global Sign Out exitoso en servidor")

        // Eliminar tokens del Keychain local
        let deleted = KeychainManager.shared.deleteTokens()
        if deleted {
            print("✅ Tokens locales eliminados del Keychain")
        } else {
            print("⚠️ No se pudieron eliminar tokens locales, pero sesión invalidada en servidor")
        }

        return "Sesión cerrada en todos los dispositivos"

    } catch {
        print("❌ Error al cerrar sesión globalmente: \(error)")

        let errorMessage = error.localizedDescription
        if errorMessage.contains("NotAuthorizedException") {
            throw NSError(domain: "CognitoAuthService", code: 9002,
                        userInfo: [NSLocalizedDescriptionKey: "Sesión inválida o expirada"])
        } else {
            throw NSError(domain: "CognitoAuthService", code: 9000,
                        userInfo: [NSLocalizedDescriptionKey: "Error al cerrar sesión: \(errorMessage)"])
        }
    }
}

Puntos clave:

✅ Requiere Access Token (usuario debe estar logueado)

✅ Llama a client.globalSignOut() – invalida tokens en servidor

✅ Elimina tokens locales DESPUÉS de invalidar en servidor

✅ Si falla eliminar local, tokens ya están invalidados en servidor

✅ Usa códigos de error 9000-9002

Paso 2: Actualizar HomeView con ambas opciones

Ahora actualizamos HomeView.swift para ofrecer ambos tipos de cierre de sesión:

struct HomeView: View {
    @Binding var isLoggedIn: Bool
    @State private var showingSignOut = false
    @State private var navigateToChangePassword = false
    @State private var isLoadingGlobalSignOut = false  // ← Nuevo
    @State private var errorMessage = ""  // ← Nuevo
    @State private var showError = false  // ← Nuevo

    private let authService = CognitoAuthService()  // ← Nuevo

    var body: some View {
        VStack(spacing: 30) {
            // ... (código existente)

            // Botón de cerrar sesión (sin cambios)
            Button(action: {
                showingSignOut = true
            }) {
                Label("Cerrar Sesión", systemImage: "arrow.right.square")
                    .fontWeight(.semibold)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(10)
            }
            .padding(.horizontal, 30)
            .padding(.bottom, 50)
        }
        .navigationBarBackButtonHidden(true)
        .navigationDestination(isPresented: $navigateToChangePassword) {
            ChangePasswordView()
        }
        // ← CAMBIO: De .alert a .confirmationDialog
        .confirmationDialog("Cerrar Sesión", isPresented: $showingSignOut, titleVisibility: .visible) {
            Button("Cerrar sesión en este dispositivo") {
                localSignOut()
            }
            Button("Cerrar sesión en TODOS los dispositivos", role: .destructive) {
                globalSignOut()
            }
            Button("Cancelar", role: .cancel) { }
        } message: {
            Text("Elige cómo quieres cerrar sesión")
        }
        // ← NUEVO: Alert de error
        .alert("Error", isPresented: $showError) {
            Button("OK", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
    }

    // Cerrar Sesión Local
    private func localSignOut() {
        // Eliminar tokens del Keychain (solo local)
        let success = KeychainManager.shared.deleteTokens()

        if success {
            print("✅ Tokens eliminados localmente. Sesión cerrada en este dispositivo.")
            print("⚠️ Los tokens siguen siendo válidos en otros dispositivos")
            // Actualizar estado de sesión (vuelve automáticamente a SignInView)
            isLoggedIn = false
        } else {
            print("❌ Error al eliminar tokens")
        }
    }

    // Cerrar Sesión Global
    private func globalSignOut() {
        isLoadingGlobalSignOut = true

        Task {
            do {
                let response = try await authService.globalSignOut()

                await MainActor.run {
                    isLoadingGlobalSignOut = false
                    print("✅ \(response)")
                    print("✅ Tokens invalidados en TODOS los dispositivos")
                    // Actualizar estado de sesión (vuelve automáticamente a SignInView)
                    isLoggedIn = false
                }
            } catch {
                await MainActor.run {
                    isLoadingGlobalSignOut = false
                    errorMessage = error.localizedDescription
                    showError = true
                    print("❌ Error en Global Sign Out: \(error.localizedDescription)")
                }
            }
        }
    }
}

Cambios clave:

✅ Cambio de .alert a .confirmationDialog

✅ Dos opciones: “este dispositivo” (local) vs “TODOS los dispositivos” (global)

✅ Opción global marcada como .destructive (roja) para indicar acción crítica

✅ Función localSignOut() – solo elimina tokens locales

✅ Función globalSignOut() – llama a API y elimina tokens

✅ Manejo de errores con alert separado

Paso 3: Probar el flujo completo

Ahora probemos el flujo:

Compilar:

xcodebuild -project CognitoAuthDemo.xcodeproj -scheme CognitoAuthDemo -destination 'platform=iOS Simulator,name=iPhone 16' clean build

Deberías ver: ** BUILD SUCCEEDED **

Escenarios de prueba

1. Local Sign Out exitoso

Pasos: 1. Inicia sesión con un usuario existente 2. En HomeView, presiona “Cerrar Sesión” 3. En el action sheet, selecciona “Cerrar sesión en este dispositivo”

Resultado esperado:

✅ Tokens eliminados del Keychain en este dispositivo

✅ Vuelve a SignInView

⚠️ Si inicias sesión en otro dispositivo, los tokens siguen siendo válidos allí

✅ Rápido (no requiere red)

Console:

✅ Tokens eliminados localmente. Sesión cerrada en este dispositivo.
⚠️ Los tokens siguen siendo válidos en otros dispositivos

2. Global Sign Out exitoso

Pasos: 1. Inicia sesión con un usuario existente 2. En HomeView, presiona “Cerrar Sesión” 3. En el action sheet, selecciona “Cerrar sesión en TODOS los dispositivos” (opción roja)

Resultado esperado:

✅ Llama a API de Cognito

✅ Tokens invalidados en servidor (todos los dispositivos)

✅ Tokens eliminados del Keychain local

✅ Vuelve a SignInView

✅ Si intentas usar tokens antiguos en otro dispositivo, fallan

Console:

✅ Global Sign Out exitoso en servidor
✅ Tokens locales eliminados del Keychain
✅ Sesión cerrada en todos los dispositivos
✅ Tokens invalidados en TODOS los dispositivos

3. Cancelar cierre de sesión

Pasos: 1. Presiona “Cerrar Sesión” 2. En el action sheet, presiona “Cancelar”

Resultado esperado:

✅ Action sheet se cierra

✅ Permanece en HomeView

✅ Sesión sigue activa

4. Global Sign Out sin conexión a internet

Pasos: 1. Desactiva WiFi y datos móviles 2. Presiona “Cerrar Sesión” 3. Selecciona “Cerrar sesión en TODOS los dispositivos”

Resultado esperado:

❌ Error de red

❌ Alert: “Error al cerrar sesión: [error de red]”

⚠️ Tokens NO se eliminan localmente (porque no se invalidaron en servidor)

✅ Permanece en HomeView

Solución alternativa: – Usuario puede usar “Cerrar sesión en este dispositivo” (funciona sin internet)

5. Global Sign Out con token expirado

Pasos: 1. Espera a que el Access Token expire (60 minutos) 2. Presiona “Cerrar Sesión” 3. Selecciona “Cerrar sesión en TODOS los dispositivos”

Resultado esperado:

❌ Alert: “Sesión inválida o expirada”

✅ Permanece en HomeView

⚠️ Tokens locales NO se eliminan (operación falló)

Nota: El token refresh debería evitar este escenario si está implementado (Artículo 6)

6. Probar invalidación en múltiples dispositivos

Pasos (requiere 2 dispositivos/simuladores): 1. Inicia sesión con el mismo usuario en Dispositivo A y Dispositivo B 2. Ambos tienen tokens válidos 3. En Dispositivo A, presiona “Cerrar Sesión” 4. Selecciona “Cerrar sesión en TODOS los dispositivos” 5. En Dispositivo B, intenta usar la app (ej: cambiar contraseña)

Resultado esperado:

✅ Dispositivo A: Sesión cerrada, vuelve a login

✅ Dispositivo B: Tokens invalidados

❌ Dispositivo B: Al intentar usar app, error “Sesión inválida”

✅ Dispositivo B: Debe volver a iniciar sesión

7. Local Sign Out no afecta otros dispositivos

Pasos (requiere 2 dispositivos/simuladores): 1. Inicia sesión con el mismo usuario en Dispositivo A y Dispositivo B 2. En Dispositivo A, presiona “Cerrar Sesión” 3. Selecciona “Cerrar sesión en este dispositivo” 4. En Dispositivo B, intenta usar la app

Resultado esperado:

✅ Dispositivo A: Sesión cerrada, vuelve a login

✅ Dispositivo B: Sesión SIGUE ACTIVA

✅ Dispositivo B: Tokens siguen siendo válidos

✅ Dispositivo B: Puede seguir usando la app normalmente

Comparación detallada: Local vs Global Sign Out

AspectoLocal Sign OutGlobal Sign Out
API de Cognito❌ No llama✅ Llama a globalSignOut()
Tokens en servidor✅ Siguen válidos❌ Invalidados
Tokens en Keychain❌ Eliminados❌ Eliminados
Otros dispositivos✅ Siguen logueados❌ Sesión cerrada
Requiere internet❌ No✅ Sí
Velocidad⚡ Instantáneo🌐 Requiere llamada API
Seguridad⚠️ Media (solo local)🔒 Alta (invalidación global)
Casos de usoCerrar sesión normalSeguridad, dispositivo perdido
Error codesN/A9000-9002

Mejores prácticas

  1. Ofrecer ambas opciones:
    • Déjale al usuario elegir según su necesidad
    • Usa confirmationDialog para mostrar opciones claras
  2. Marcar Global como destructive:
    • Usa role: .destructive para opción global (roja)
    • Indica que es una acción más crítica
  3. Manejo de errores de red:
    • Global Sign Out puede fallar sin internet
    • Informa al usuario y ofrece Local Sign Out como alternativa
  4. Logging claro:
    • Diferencia logs entre Local y Global
    • Ayuda en debugging y auditoría
  5. Orden de operaciones:
    • PRIMERO invalida en servidor (global)
    • DESPUÉS elimina tokens locales
    • Si falla servidor, NO elimines local (tokens quedan útiles)
  6. UX:
    • Título descriptivo: “Cerrar sesión en este dispositivo” vs “TODOS los dispositivos”
    • Mensaje explicativo: “Elige cómo quieres cerrar sesión”

Errores comunes y soluciones

Error 1: “Sesión inválida o expirada”

Causa: Access Token ya expiró antes de llamar a globalSignOut Solución: – Implementar token refresh automático (Artículo 6) – O permitir que usuario use Local Sign Out si falla Global

Error 2: Global Sign Out no funciona sin internet

Causa: Requiere llamada a API de Cognito Solución: – Detectar falta de conexión – Ofrecer Local Sign Out como alternativa – Informar al usuario claramente

Error 3: Tokens locales no se eliminan después de Global Sign Out

Causa: Error en la lógica – eliminar antes de validar respuesta del servidor Solución: – SIEMPRE elimina tokens DESPUÉS de recibir confirmación del servidor – Si falla API, NO elimines tokens locales

Error 4: Usuario cierra local pero sigue recibiendo notificaciones push

Causa: Tokens de push no se desregistraron Solución: – En Local/Global Sign Out, también desregistrar push tokens – Llamar a UNUserNotificationCenter.current().removeAllPendingNotificationRequests()

¿Qué sigue?

Has completado el flujo de Global Sign Out. Ahora los usuarios pueden: – ✅ Cerrar sesión localmente (rápido, solo este dispositivo) – ✅ Cerrar sesión globalmente (seguro, todos los dispositivos) – ✅ Elegir la opción según su necesidad

En el siguiente artículo implementaremos Get User Attributes, que permite obtener información del usuario autenticado (email, sub, custom attributes, etc.).

Resumen

En este artículo implementamos:

  1. Método globalSignOut en CognitoAuthService
    • Llama a API de Cognito para invalidar tokens en servidor
    • Elimina tokens locales DESPUÉS de invalidar en servidor
    • Error codes: 9000-9002
  2. Actualización de HomeView
    • Cambio de .alert a .confirmationDialog
    • Dos opciones: Local vs GlobalFunción localSignOut() – solo local
    • Función globalSignOut() – invalidación global
  3. Diferenciación clara
    • Local: Rápido, solo este dispositivo, sin internet
    • Global: Seguro, todos los dispositivos, requiere internet
  4. Manejo de errores
    • Error de red
    • Token expirado
    • Alert informativo
  5. Testing multi-dispositivo
    • Validar que Global invalida en todos lados
    • Validar que Local solo afecta un dispositivo

Diferencia clave:Local Sign Out: Solo elimina tokens locales, NO llama a API – Global Sign Out: Invalida tokens en servidor + elimina locales

Sobre el autor

José Luján es un desarrollador móvil especializado en iOS, Android y desarrollo multiplataforma (Flutter). Experto en integración de servicios AWS en aplicaciones móviles y autor de múltiples artículos técnicos. También forma parte del programa Amazon Community Builders.

Conéctate conmigo: – LinkedIn: https://www.linkedin.com/in/josed-lujan/ – GitHub: https://github.com/josedlucas – Beacons: https://beacons.ai/josedlujan

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *