---
title: Validate JWT đầy đủ - kiểm tra signature, issuer, audience và expiry
impact: CRITICAL
impactDescription: Chấp nhận JWT chỉ dựa vào decode mà không verify signature cho phép attacker forge token với bất kỳ claims nào. Missing audience/issuer check cho phép token của service khác được dùng.
tags: swift, ios, jwt, token-validation, signature, claims, security
---

## Validate JWT đầy đủ - kiểm tra signature, issuer, audience và expiry

JWT phải được validate server-side. Client-side chỉ nên đọc claims để biết expiry, user ID cho UX - không dùng claims để ra quyết định security. Khi nhận JWT từ server, phải đảm bảo server đã validate đầy đủ trước khi trust bất kỳ claim nào.

**Incorrect (decode JWT mà không verify, dùng claims cho security decision):**

```swift
import Foundation

struct JWTDecoder {
    // !! Decode không verify signature
    static func decode(token: String) -> [String: Any]? {
        let parts = token.components(separatedBy: ".")
        guard parts.count == 3 else { return nil }
        var base64 = parts[1]
        let padded = base64 + String(repeating: "=", count: (4 - base64.count % 4) % 4)
        guard let data = Data(base64Encoded: padded) else { return nil }
        return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
    }
}

class AuthorizationChecker {
    // !! Dùng JWT claims (không verify) để kiểm tra quyền - attacker có thể forge!
    func isAdmin(token: String) -> Bool {
        let claims = JWTDecoder.decode(token: token)
        return claims?["role"] as? String == "admin"  // Không verify signature!
    }

    // !! Không check issuer/audience - token của service khác có thể dùng được
    func validateToken(_ token: String) -> Bool {
        guard let claims = JWTDecoder.decode(token: token),
              let exp = claims["exp"] as? TimeInterval else { return false }
        return Date().timeIntervalSince1970 < exp  // Chỉ check expiry!
        // Không check: iss (issuer), aud (audience), alg, nbf
    }
}
```

**Correct (validate server-side, client chỉ đọc claims cho UX):**

```swift
import Foundation

// SAFE: Server-side validation là bắt buộc. Client chỉ đọc non-security claims.
struct JWTClaims: Decodable {
    let sub: String       // Subject (user ID)
    let exp: TimeInterval // Expiry
    let iat: TimeInterval // Issued at
    let iss: String       // Issuer - phải match expected value
    let aud: String       // Audience - phải match app's client_id
    let jti: String?      // JWT ID - để detect reuse nếu cần
}

struct ClientSideJWTReader {
    private let expectedIssuer: String
    private let expectedAudience: String

    init(issuer: String, audience: String) {
        self.expectedIssuer = issuer
        self.expectedAudience = audience
    }

    // Client-side decode CHỈ để đọc UX data (user ID, expiry)
    // KHÔNG dùng cho security decision - server phải verify signature
    func readClaimsForUX(from token: String) throws -> JWTClaims {
        let parts = token.components(separatedBy: ".")
        guard parts.count == 3 else { throw JWTError.malformedToken }

        let base64 = parts[1]
        let padded = base64 + String(repeating: "=", count: (4 - base64.count % 4) % 4)
        guard let data = Data(base64Encoded: padded) else { throw JWTError.malformedToken }

        let decoder = JSONDecoder()
        let claims = try decoder.decode(JWTClaims.self, from: data)

        // Validate claims cơ bản cho UX (không thay thế server validation)
        guard claims.iss == expectedIssuer else {
            throw JWTError.invalidIssuer(claims.iss)
        }
        guard claims.aud == expectedAudience else {
            throw JWTError.invalidAudience(claims.aud)
        }
        guard Date().timeIntervalSince1970 < claims.exp else {
            throw JWTError.tokenExpired
        }

        // NOTE: Signature chưa được verify ở đây!
        // Tất cả API request phải gửi token để SERVER verify signature
        return claims
    }
}

class AuthManager {
    private let jwtReader = ClientSideJWTReader(
        issuer: "https://auth.example.com",
        audience: "com.example.myapp"
    )

    // SAFE: Server verify full JWT, client chỉ dùng userId từ claims để hiển thị UI
    func setupAfterLogin(accessToken: String) throws {
        let claims = try jwtReader.readClaimsForUX(from: accessToken)
        // Chỉ dùng sub (user ID) để fetch profile UI, không phải security check
        currentUserId = claims.sub
        tokenExpiryDate = Date(timeIntervalSince1970: claims.exp)
    }

    // SAFE: Authorization check thông qua server API, không phải local JWT claims
    func checkAdminAccess() async throws -> Bool {
        let token = try KeychainService.readToken(key: "access_token")
        // Server endpoint sẽ verify JWT và check role
        var request = URLRequest(url: URL(string: "https://api.example.com/admin/verify")!)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode == 200
    }
}

enum JWTError: LocalizedError {
    case malformedToken, invalidIssuer(String), invalidAudience(String), tokenExpired

    var errorDescription: String? {
        switch self {
        case .malformedToken: return "Malformed JWT token"
        case .invalidIssuer(let iss): return "Invalid issuer: \(iss)"
        case .invalidAudience(let aud): return "Invalid audience: \(aud)"
        case .tokenExpired: return "Token has expired"
        }
    }
}
```

**Tools:** JWTDecode.swift, Auth0 SDK, OWASP MASVS-AUTH-1, jwt.io (debug)
