Global Sign Out with AWS Cognito in iOS

In the previous articles, we implemented authentication, password recovery, and password change. We also have a «Sign Out» button that only deletes tokens locally. In this article, we’ll implement Global Sign Out, which invalidates tokens on ALL user devices simultaneously.

What are we building?

  • globalSignOut method — Invalidate tokens on all devices
  • Update HomeView — Offer two options: Local Sign Out vs Global
  • Differentiate both flows — Local (fast) vs Global (secure)
  • Error handling — Invalid token, expired session
  • Testing — Test sign out in multiple scenarios

Difference: Local Sign Out vs Global Sign Out

AspectLocal Sign Out (Article 3)Global Sign Out (this article)
Cognito API call❌ No✅ Yes — globalSignOut()
Tokens on other devices✅ Remain valid❌ Invalidated
Local Keychain tokens❌ Deleted❌ Deleted
Requires internet❌ No✅ Yes
Speed⚡ Instant🌐 API call
Security level⚠️ Medium🔒 High

The Global Sign Out flow

User in HomeView → presses «Global Sign Out» → confirmation alert → confirms → globalSignOut(accessToken) called → Cognito invalidates tokens server-side → local tokens deleted from Keychain → returns to SignInView.

Use cases:

  • Local Sign Out: User only wants to sign out on this device, continues using app on other devices, quick sign out
  • Global Sign Out: User suspects account was compromised, lost a device with active session, wants maximum security, changing devices permanently

Prerequisites

  • Have completed previous articles (especially Article 3: Login)
  • Have CognitoAuthService implemented
  • Have HomeView with «Sign Out» button
  • User logged in with tokens in Keychain

Step 1: Add globalSignOut Method in CognitoAuthService

Add the method in CognitoAuthService.swift:

// MARK: - Global Sign Out
/// Signs out the user on ALL devices
/// Invalidates tokens on Cognito server and deletes local tokens
func globalSignOut() async throws {
    guard let accessToken = KeychainService.shared.getAccessToken() else {
        throw NSError(domain: "CognitoAuthService", code: 9001,
                     userInfo: [NSLocalizedDescriptionKey: "Invalid or expired session"])
    }
    guard let client = cognitoClient else {
        throw NSError(domain: "CognitoAuthService", code: -1,
                     userInfo: [NSLocalizedDescriptionKey: "Cognito client not initialized"])
    }
    let signOutInput = GlobalSignOutInput(accessToken: accessToken)
    do {
        _ = try await client.globalSignOut(input: signOutInput)
        print("✅ Global Sign Out successful on server")
        // Delete local tokens AFTER successful server invalidation
        KeychainService.shared.deleteTokens()
        print("✅ Local tokens deleted from Keychain")
        print("✅ Session closed on all devices")
        print("✅ Tokens invalidated on Cognito server")
    } catch {
        print("❌ Error in Global Sign Out: \(error)")
        let errorMessage = error.localizedDescription
        if errorMessage.contains("NotAuthorizedException") {
            throw NSError(domain: "CognitoAuthService", code: 9002,
                        userInfo: [NSLocalizedDescriptionKey: "Invalid or expired session"])
        } else {
            throw NSError(domain: "CognitoAuthService", code: 9000,
                        userInfo: [NSLocalizedDescriptionKey: "Error signing out: \(errorMessage)"])
        }
    }
}

Key points:

  • ✅ Requires Access Token
  • ✅ Calls client.globalSignOut() — invalidates tokens on Cognito server
  • ✅ FIRST invalidates on server, THEN deletes local tokens
  • ✅ Uses error codes 9000-9002

Step 2: Update HomeView with Both Options

Update HomeView.swift to offer both types of sign out:

struct HomeView: View {
    @Binding var isLoggedIn: Bool
    @State private var showingSignOut = false
    @State private var showingGlobalSignOutAlert = false
    @State private var isSigningOut = false
    @State private var errorMessage = ""
    @State private var showError = false
    private let authService = CognitoAuthService()

    var body: some View {
        VStack(spacing: 20) {
            // ... welcome content ...

            // Sign Out button (shows action sheet)
            Button(action: { showingSignOut = true }) {
                Text("Sign Out")
                    .fontWeight(.semibold)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
            }
            .background(Color.red)
            .cornerRadius(10)
            .padding(.horizontal, 30)
        }
        .confirmationDialog("Choose how you want to sign out",
                           isPresented: $showingSignOut,
                           titleVisibility: .visible) {
            Button("Sign out on this device") {
                localSignOut()
            }
            Button("Sign out on ALL devices", role: .destructive) {
                Task { await performGlobalSignOut() }
            }
            Button("Cancel", role: .cancel) { }
        }
        .alert("Error", isPresented: $showError) {
            Button("OK", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
    }

    private func localSignOut() {
        KeychainService.shared.deleteTokens()
        print("✅ Tokens deleted locally. Session closed on this device.")
        print("⚠️ Tokens remain valid on other devices")
        isLoggedIn = false
    }

    private func performGlobalSignOut() async {
        isSigningOut = true
        do {
            try await authService.globalSignOut()
            await MainActor.run {
                isSigningOut = false
                isLoggedIn = false
            }
        } catch {
            await MainActor.run {
                isSigningOut = false
                errorMessage = error.localizedDescription
                showError = true
            }
        }
    }
}

Key changes:

  • ✅ Changed from .alert to .confirmationDialog — shows options in action sheet
  • ✅ Two options: «Sign out on this device» (local) and «Sign out on ALL devices» (global, destructive role = red)
  • ✅ localSignOut() — only deletes local Keychain tokens
  • ✅ performGlobalSignOut() — calls Cognito API then deletes local tokens
  • ✅ Error handling with separate alert
HomeView with confirmationDialog showing Local and Global Sign Out options

Step 3: Test the Complete Flow

Test 1: Successful Local Sign Out

Sign in → HomeView → press «Sign Out» → action sheet appears → select «Sign out on this device» → console: «✅ Tokens deleted locally. Session closed on this device.» → returns to SignInView. Note: tokens remain valid on other devices.

Test 2: Successful Global Sign Out

Sign in → HomeView → press «Sign Out» → action sheet → select «Sign out on ALL devices» → console: «✅ Global Sign Out successful on server → ✅ Local tokens deleted → ✅ Session closed on all devices → ✅ Tokens invalidated on Cognito server» → returns to SignInView. Tokens are now invalid on ALL devices.

Test 3: Cancel sign out

Press «Sign Out» → action sheet → press «Cancel» → action sheet closes → remains in HomeView → session stays active.

Test 4: Global Sign Out without internet

Turn off WiFi and mobile data → press «Sign Out» → select «Sign out on ALL devices» → network error → alert: «Error signing out: [network error]» → tokens NOT deleted locally (because server invalidation failed). Alternative: use «Sign out on this device» (works without internet).

Test 5: Global Sign Out with expired token

Wait for Access Token to expire (60 min) → press «Sign Out» → select «Sign out on ALL devices» → alert: «Invalid or expired session» → remains in HomeView → local tokens NOT deleted. Note: token refresh (Article 6) should prevent this scenario.

Test 6: Multi-device invalidation

Sign in on Device A and Device B → both have valid tokens → on Device A: select «Sign out on ALL devices» → Device A: returns to SignInView → Device B: tokens invalidated → Device B: next API call returns 401 → should redirect to sign in.

Test 7: Local Sign Out doesn’t affect other devices

Sign in on Device A and Device B → on Device A: select «Sign out on this device» → Device A: returns to login → Device B: session REMAINS ACTIVE, tokens still valid.

Detailed comparison: Local vs Global Sign Out

AspectLocalGlobal
Cognito API❌ No✅ globalSignOut()
Tokens on server✅ Remain valid❌ Invalidated
Tokens in Keychain❌ Deleted❌ Deleted
Other devices✅ Stay logged in❌ Session closed
Requires internet❌ No✅ Yes
Speed⚡ Instant🌐 API call
Security⚠️ Medium🔒 High
Error codesN/A9000-9002

Best practices

Offer both options: let user choose based on their need, use confirmationDialog for clear options.

Mark Global as destructive: use role: .destructive (red), indicates it’s a more critical action.

Handle network errors: global sign out can fail without internet, inform user and offer local sign out as alternative.

Order of operations: FIRST invalidate on server, THEN delete local tokens. If server fails, do NOT delete local tokens.

Common errors and solutions

Error 1: «Invalid or expired session»

Cause: Access Token expired. Solution: implement automatic token refresh (Article 6), or try local sign out as fallback.

Error 2: Global Sign Out fails without internet

Cause: requires Cognito API call. Solution: detect lack of connection, offer Local Sign Out as alternative.

Error 3: Local tokens not deleted after Global Sign Out

Cause: deleting before validating server response. Solution: always delete tokens AFTER receiving successful server response.

Error 4: User still receives push notifications after sign out

Cause: push tokens not deregistered. Solution: in Local/Global Sign Out, also deregister push tokens (UNUserNotificationCenter).

Summary

In this article, we implemented:

  • ✅ globalSignOut method — calls Cognito API, deletes local tokens AFTER server invalidation, error codes 9000-9002
  • ✅ HomeView update — confirmationDialog with two options (Local vs Global), localSignOut() and globalSignOut() functions
  • ✅ Clear differentiation — Local: fast, this device, no internet needed. Global: secure, all devices, requires internet
  • ✅ Error handling — network error, expired token, informative alerts
  • ✅ Multi-device testing — Global invalidates everywhere, Local only affects one device

Key difference: Local Sign Out only deletes local tokens (does NOT call API). Global Sign Out calls globalSignOut() API and invalidates tokens on ALL devices.

Final reflection

I invite you to follow me on LinkedIn where I share content related to software development, best practices, and AWS technologies. You can also find me on Medium and explore my projects on GitHub. If you have questions or suggestions, don’t hesitate to connect.

Disclaimer

This article is for educational purposes. In production, you should: implement HTTPS for all API calls, validate tokens before using them, handle token expiration gracefully, implement push notification deregistration on sign out, comprehensive testing for multi-device scenarios.

Deja una respuesta

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