TokenModel.swift
9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//
// TokenModel.swift
// SwiftWarplyFramework
//
// Created by Manos Chorianopoulos on 24/6/25.
//
import Foundation
/// TokenModel represents OAuth tokens with JWT parsing capabilities
/// This model handles token lifecycle management, expiration detection, and validation
struct TokenModel {
let accessToken: String
let refreshToken: String
let clientId: String?
let clientSecret: String?
let expirationDate: Date?
// MARK: - Token Lifecycle Management
/// Check if the access token is currently expired
var isExpired: Bool {
guard let expirationDate = expirationDate else {
// If we can't parse expiration, assume token is still valid
return false
}
return Date() >= expirationDate
}
/// Check if the token should be refreshed proactively (5 minutes before expiry)
var shouldRefresh: Bool {
guard let expirationDate = expirationDate else {
// If we can't parse expiration, don't refresh proactively
return false
}
// Refresh 5 minutes (300 seconds) before expiration
return Date().addingTimeInterval(300) >= expirationDate
}
/// Validate token format and structure
var isValid: Bool {
return !accessToken.isEmpty &&
!refreshToken.isEmpty &&
isValidJWTFormat(accessToken)
}
/// Get time until token expires (in seconds)
var timeUntilExpiration: TimeInterval? {
guard let expirationDate = expirationDate else { return nil }
return expirationDate.timeIntervalSinceNow
}
}
// MARK: - JWT Parsing Extension
extension TokenModel {
/// Parse JWT expiration date from access token
/// JWT structure: header.payload.signature (Base64 URL encoded)
static func parseJWTExpiration(from token: String) -> Date? {
print("🔍 [TokenModel] Parsing JWT expiration from token")
// JWT structure: header.payload.signature
let components = token.components(separatedBy: ".")
guard components.count == 3 else {
print("⚠️ [TokenModel] Invalid JWT format - expected 3 components, got \(components.count)")
return nil
}
// Decode payload (second component)
let payload = components[1]
guard let data = base64UrlDecode(payload) else {
print("⚠️ [TokenModel] Failed to decode JWT payload")
return nil
}
// Parse JSON payload
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
if let exp = json["exp"] as? TimeInterval {
let expirationDate = Date(timeIntervalSince1970: exp)
print("✅ [TokenModel] JWT expiration parsed: \(expirationDate)")
return expirationDate
} else {
print("⚠️ [TokenModel] No 'exp' claim found in JWT payload")
}
} else {
print("⚠️ [TokenModel] JWT payload is not a valid JSON object")
}
} catch {
print("❌ [TokenModel] JWT parsing error: \(error)")
}
return nil
}
/// Base64 URL decode (JWT uses URL-safe Base64 without padding)
private static func base64UrlDecode(_ string: String) -> Data? {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
// Add padding if needed (Base64 requires length to be multiple of 4)
let remainder = base64.count % 4
if remainder > 0 {
base64 += String(repeating: "=", count: 4 - remainder)
}
return Data(base64Encoded: base64)
}
}
// MARK: - Validation Methods
extension TokenModel {
/// Check if token follows JWT format (3 parts separated by dots)
private func isValidJWTFormat(_ token: String) -> Bool {
let components = token.components(separatedBy: ".")
return components.count == 3 &&
components.allSatisfy { !$0.isEmpty }
}
/// Get formatted expiration info for debugging
var expirationInfo: String {
guard let expirationDate = expirationDate else {
return "No expiration date available"
}
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
if isExpired {
return "Expired at \(formatter.string(from: expirationDate))"
} else if shouldRefresh {
return "Should refresh - expires at \(formatter.string(from: expirationDate))"
} else {
return "Valid until \(formatter.string(from: expirationDate))"
}
}
/// Get time remaining until expiration in a human-readable format
var timeRemainingDescription: String {
guard let timeRemaining = timeUntilExpiration else {
return "Unknown"
}
if timeRemaining <= 0 {
return "Expired"
}
let hours = Int(timeRemaining) / 3600
let minutes = Int(timeRemaining.truncatingRemainder(dividingBy: 3600)) / 60
let seconds = Int(timeRemaining.truncatingRemainder(dividingBy: 60))
if hours > 0 {
return "\(hours)h \(minutes)m \(seconds)s"
} else if minutes > 0 {
return "\(minutes)m \(seconds)s"
} else {
return "\(seconds)s"
}
}
}
// MARK: - Convenience Initializers
extension TokenModel {
/// Initialize with automatic JWT expiration parsing
init(accessToken: String, refreshToken: String, clientId: String? = nil, clientSecret: String? = nil) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.clientId = clientId ?? "" // Legacy credential - always use empty string
self.clientSecret = clientSecret ?? "" // Legacy credential - always use empty string
self.expirationDate = Self.parseJWTExpiration(from: accessToken)
print("🔐 [TokenModel] Created token model - \(expirationInfo)")
}
/// Initialize from database values (returns nil if required tokens are missing)
init?(accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?) {
guard let accessToken = accessToken, !accessToken.isEmpty,
let refreshToken = refreshToken, !refreshToken.isEmpty else {
print("⚠️ [TokenModel] Cannot create token model - missing required tokens")
return nil
}
self.init(
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
clientSecret: clientSecret
)
}
/// Create a new TokenModel with updated tokens (preserves client credentials)
func withUpdatedTokens(accessToken: String, refreshToken: String) -> TokenModel {
return TokenModel(
accessToken: accessToken,
refreshToken: refreshToken,
clientId: self.clientId,
clientSecret: self.clientSecret
)
}
}
// MARK: - Debug and Logging Support
extension TokenModel {
/// Safe description for logging (doesn't expose sensitive data)
var debugDescription: String {
let accessTokenPreview = String(accessToken.prefix(10)) + "..."
let refreshTokenPreview = String(refreshToken.prefix(10)) + "..."
return """
TokenModel {
accessToken: \(accessTokenPreview)
refreshToken: \(refreshTokenPreview)
clientId: \(clientId ?? "nil")
hasClientSecret: \(clientSecret != nil)
expirationDate: \(expirationDate?.description ?? "nil")
isExpired: \(isExpired)
shouldRefresh: \(shouldRefresh)
isValid: \(isValid)
timeRemaining: \(timeRemainingDescription)
}
"""
}
/// Detailed token status for debugging
var statusDescription: String {
if !isValid {
return "❌ Invalid token format"
} else if isExpired {
return "🔴 Token expired"
} else if shouldRefresh {
return "🟡 Token should be refreshed"
} else {
return "🟢 Token is valid"
}
}
}
// MARK: - Database Integration Helpers
extension TokenModel {
/// Convert to tuple format for database storage
var databaseValues: (accessToken: String, refreshToken: String, clientId: String?, clientSecret: String?) {
return (accessToken, refreshToken, clientId, clientSecret)
}
/// Create from database tuple
static func fromDatabaseValues(_ values: (accessToken: String?, refreshToken: String?, clientId: String?, clientSecret: String?)) -> TokenModel? {
return TokenModel(
accessToken: values.accessToken,
refreshToken: values.refreshToken,
clientId: values.clientId,
clientSecret: values.clientSecret
)
}
}
// MARK: - Token Refresh Support
extension TokenModel {
/// Check if this token can be used for refresh (has refresh token and client credentials)
var canRefresh: Bool {
return !refreshToken.isEmpty &&
clientId != nil &&
clientSecret != nil
}
/// Get refresh request parameters
var refreshParameters: [String: String]? {
guard let clientId = clientId,
let clientSecret = clientSecret else {
print("⚠️ [TokenModel] Cannot create refresh parameters - missing client credentials")
return nil
}
return [
"client_id": clientId,
"client_secret": clientSecret,
"refresh_token": refreshToken,
"grant_type": "refresh_token"
]
}
}