---
title: Xóa token và invalidate session hoàn toàn khi logout
impact: HIGH
impactDescription: Logout không xóa token khỏi Keychain hoặc không revoke token trên server cho phép tái sử dụng token cũ để truy cập API, đặc biệt nguy hiểm khi thiết bị bị mất hoặc chia sẻ.
tags: swift, ios, logout, session-invalidation, keychain, token-revocation, security
---

## Xóa token và invalidate session hoàn toàn khi logout

Logout phải thực hiện đầy đủ 4 bước: (1) revoke token trên server, (2) xóa tất cả token khỏi Keychain, (3) xóa cache nhạy cảm trong memory và disk, (4) reset navigation stack về login screen. Không được chỉ navigate về login mà token vẫn còn.

**Incorrect (logout không đầy đủ):**

```swift
import UIKit

class SettingsViewController: UIViewController {

    // !! Chỉ navigate về login, không xóa token
    @IBAction func logoutTapped(_ sender: UIButton) {
        let loginVC = LoginViewController()
        navigationController?.setViewControllers([loginVC], animated: true)
        // Token vẫn còn trong UserDefaults/Keychain!
    }
}

class AuthManager {
    // !! Chỉ xóa UserDefaults, bỏ sót Keychain
    func logout() {
        UserDefaults.standard.removeObject(forKey: "access_token")  // Sai: token nên chỉ ở Keychain
        // Không revoke token trên server!
        // Không xóa Keychain entries!
        // Không xóa URLSession cookie!
    }
}
```

**Correct (logout đầy đủ):**

```swift
import UIKit

class AuthManager {
    private let keychainService = KeychainService.self
    private let tokenKeys = ["access_token", "refresh_token", "id_token"]

    // SAFE: Logout hoàn toàn
    func logout() async {
        // Bước 1: Revoke token trên server (best effort, không block logout nếu fail)
        if let refreshToken = try? keychainService.readToken(key: "refresh_token") {
            await revokeTokenOnServer(refreshToken: refreshToken)
        }

        // Bước 2: Xóa tất cả credentials khỏi Keychain
        tokenKeys.forEach { key in
            keychainService.deleteToken(key: key)
        }

        // Bước 3: Xóa URLSession cookies và cache
        URLSession.shared.configuration.urlCache?.removeAllCachedResponses()
        HTTPCookieStorage.shared.cookies?.forEach {
            HTTPCookieStorage.shared.deleteCookie($0)
        }

        // Bước 4: Xóa sensitive data trong UserDefaults
        let sensitiveKeys = ["user_profile_cache", "last_search_query"]
        sensitiveKeys.forEach { UserDefaults.standard.removeObject(forKey: $0) }

        // Bước 5: Navigate về login ở main thread
        await MainActor.run {
            self.resetToLoginScreen()
        }
    }

    private func revokeTokenOnServer(refreshToken: String) async {
        guard let url = URL(string: "https://api.example.com/auth/revoke") else { return }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        _ = try? await URLSession.shared.data(for: request)
    }

    private func resetToLoginScreen() {
        guard let window = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first?.windows.first else { return }

        let loginVC = UINavigationController(rootViewController: LoginViewController())
        window.rootViewController = loginVC
        window.makeKeyAndVisible()
    }
}

// Gọi khi app nhận được 401 Unauthorized
extension AuthManager {
    func handleUnauthorizedResponse() {
        Task { await logout() }
    }
}
```

**Tools:** Proxyman (verify token unusable after logout), OWASP MASVS-AUTH-4, Instruments (memory inspection)
