---
title: Xử lý token hết hạn và implement silent refresh - không để user bị logout đột ngột
impact: MEDIUM
impactDescription: Không kiểm tra expiry trước khi dùng token dẫn đến 401 errors gây UX xấu, hoặc ngược lại không refresh token đúng cách dẫn đến sử dụng token đã hết hạn mà không detect.
tags: swift, ios, token-expiry, refresh-token, jwt, oauth, api-security
---

## Xử lý token hết hạn và implement silent refresh - không để user bị logout đột ngột

Kiểm tra JWT expiry (`exp` claim) trước mỗi request quan trọng. Implement silent refresh: khi access token sắp hết hạn (ví dụ còn <5 phút), dùng refresh token để lấy token mới tự động. Khi refresh token cũng hết hạn, mới force logout.

**Incorrect (không kiểm tra expiry):**

```swift
import Foundation

class APIClient {
    // !! Dùng token mà không kiểm tra còn hạn không
    func fetchUserProfile() async throws -> UserProfile {
        let token = try KeychainService.readToken(key: "access_token")
        var request = URLRequest(url: URL(string: "https://api.example.com/profile")!)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(UserProfile.self, from: data)
        // Nếu 401 → crash hoặc hiển thị error mà không cố refresh
    }
}
```

**Correct (proactive token check và silent refresh):**

```swift
import Foundation

struct JWTTokenManager {
    // Parse JWT claims mà không verify signature (signature verify ở server)
    static func expiryDate(from jwtToken: String) -> Date? {
        let parts = jwtToken.components(separatedBy: ".")
        guard parts.count == 3 else { return nil }

        var base64 = parts[1]
        // Pad base64
        let padded = base64.count % 4 == 0 ? base64 : base64 + String(repeating: "=", count: 4 - base64.count % 4)
        guard let payloadData = Data(base64Encoded: padded),
              let payload = try? JSONDecoder().decode([String: AnyCodable].self, from: payloadData),
              let exp = payload["exp"]?.value as? TimeInterval else { return nil }
        return Date(timeIntervalSince1970: exp)
    }

    static func isTokenExpiringSoon(_ token: String, withinSeconds: TimeInterval = 300) -> Bool {
        guard let expiry = expiryDate(from: token) else { return true }
        return expiry.timeIntervalSinceNow < withinSeconds
    }
}

actor TokenRefreshManager {
    private var refreshTask: Task<String, Error>?

    // SAFE: Single refresh task để tránh thundering herd
    func getValidAccessToken() async throws -> String {
        let currentToken = try KeychainService.readToken(key: "access_token")

        // Nếu token còn hạn đủ dùng, return luôn
        if !JWTTokenManager.isTokenExpiringSoon(currentToken) {
            return currentToken
        }

        // Nếu đang refresh, chờ task hiện tại
        if let existing = refreshTask {
            return try await existing.value
        }

        // Tạo refresh task mới
        let task = Task<String, Error> {
            defer { self.refreshTask = nil }
            return try await performTokenRefresh()
        }
        self.refreshTask = task
        return try await task.value
    }

    private func performTokenRefresh() async throws -> String {
        guard let refreshToken = try? KeychainService.readToken(key: "refresh_token") else {
            throw TokenError.noRefreshToken
        }

        var request = URLRequest(url: URL(string: "https://api.example.com/auth/refresh")!)
        request.httpMethod = "POST"
        request.httpBody = try? JSONEncoder().encode(["refresh_token": refreshToken])
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw TokenError.networkError
        }

        if httpResponse.statusCode == 401 {
            // Refresh token hết hạn → force logout
            throw TokenError.sessionExpired
        }

        let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
        try KeychainService.saveToken(tokenResponse.accessToken, key: "access_token")
        if let newRefresh = tokenResponse.refreshToken {
            try KeychainService.saveToken(newRefresh, key: "refresh_token")
        }
        return tokenResponse.accessToken
    }
}

class APIClient {
    private let tokenManager = TokenRefreshManager()

    // SAFE: Auto-refresh trước khi dùng token
    func fetchUserProfile() async throws -> UserProfile {
        do {
            let token = try await tokenManager.getValidAccessToken()
            var request = URLRequest(url: URL(string: "https://api.example.com/profile")!)
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            let (data, _) = try await URLSession.shared.data(for: request)
            return try JSONDecoder().decode(UserProfile.self, from: data)
        } catch TokenError.sessionExpired {
            // Xử lý logout khi session thực sự hết hạn
            await AuthManager.shared.logout()
            throw TokenError.sessionExpired
        }
    }
}

enum TokenError: Error { case noRefreshToken, networkError, sessionExpired }
struct TokenResponse: Decodable { let accessToken: String; let refreshToken: String? }
```

**Tools:** JWTDecode.swift (library), OWASP MASVS-AUTH-3, OAuth 2.0 RFC 6749
