Skip to content

Commit ba95d48

Browse files
authored
Merge pull request #7 from augard/refresh-token
Implement proper OAuth2 refresh token flow
2 parents 1a03755 + 66f68d2 commit ba95d48

6 files changed

Lines changed: 119 additions & 41 deletions

File tree

KiaMaps/App/ApiExtensions.swift

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,52 @@ extension Api {
4141
}
4242
}
4343

44-
/// Attempts to refresh the token using stored credentials
44+
/// Attempts to refresh the token using stored refresh token or fallback to credentials
4545
/// - returns: True if token was successfully refreshed, false otherwise
4646
private func refreshTokenIfPossible() async -> Bool {
47-
// Check if we have stored login credentials
48-
guard let storedCredentials = LoginCredentialManager.retrieveCredentials() else {
49-
logInfo("No stored credentials available for token refresh", category: .auth)
50-
return false
51-
}
52-
5347
// Check if current token is actually expired before attempting refresh
5448
if let currentAuth = authorization, !isTokenExpired(currentAuth) {
5549
logDebug("Current token is not expired, no refresh needed", category: .auth)
5650
return true
5751
}
5852

53+
// First, try to refresh using the refresh token if available
54+
if let currentAuth = authorization {
55+
logInfo("Attempting to refresh token using refresh token", category: .auth)
56+
57+
do {
58+
let tokenResponse = try await refreshToken(currentAuth.refreshToken)
59+
60+
// Create new authorization data with refreshed tokens
61+
let newAuthData = AuthorizationData(
62+
stamp: currentAuth.stamp, // Keep existing stamp
63+
deviceId: currentAuth.deviceId, // Keep existing device ID
64+
accessToken: tokenResponse.accessToken,
65+
expiresIn: tokenResponse.expiresIn,
66+
refreshToken: tokenResponse.refreshToken ?? currentAuth.refreshToken,
67+
isCcuCCS2Supported: currentAuth.isCcuCCS2Supported
68+
)
69+
70+
// Store the new authorization data
71+
Authorization.store(data: newAuthData)
72+
self.authorization = newAuthData
73+
74+
logInfo("Successfully refreshed token using refresh token", category: .auth)
75+
return true
76+
} catch {
77+
logWarning("Refresh token failed: \(error.localizedDescription). Falling back to credential login", category: .auth)
78+
// Continue to fallback method below
79+
}
80+
}
81+
82+
// Fallback: Try to refresh using stored login credentials
83+
guard let storedCredentials = LoginCredentialManager.retrieveCredentials() else {
84+
logInfo("No stored credentials available for fallback token refresh", category: .auth)
85+
return false
86+
}
87+
5988
do {
60-
logInfo("Attempting to login with stored credentials", category: .auth)
89+
logInfo("Attempting fallback login with stored credentials", category: .auth)
6190
let newAuthData = try await login(
6291
username: storedCredentials.username,
6392
password: storedCredentials.password
@@ -67,11 +96,11 @@ extension Api {
6796
Authorization.store(data: newAuthData)
6897
self.authorization = newAuthData
6998

70-
logInfo("Successfully refreshed token", category: .auth)
99+
logInfo("Successfully refreshed token using credential fallback", category: .auth)
71100
return true
72101

73102
} catch {
74-
logError("Failed to refresh token with error: \(error.localizedDescription)", category: .auth)
103+
logError("Failed to refresh token with fallback login: \(error.localizedDescription)", category: .auth)
75104

76105
// If login fails, clear the stored credentials as they might be invalid
77106
if error is ApiError {

KiaMaps/Core/Api/Api.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class Api {
161161
deviceId: deviceId,
162162
accessToken: tokenResponse.accessToken,
163163
expiresIn: tokenResponse.expiresIn,
164-
refreshToken: tokenResponse.refreshToken,
164+
refreshToken: tokenResponse.refreshToken ?? "",
165165
isCcuCCS2Supported: true
166166
)
167167

@@ -564,6 +564,23 @@ extension Api {
564564
).data()
565565
}
566566

567+
/// Login - Refresh token
568+
func refreshToken(_ refreshToken: String) async throws -> TokenResponse {
569+
let form: [String: String] = [
570+
"client_id": configuration.serviceId,
571+
"client_secret": "secret", // TODO: something generated
572+
"grant_type": "refresh_token",
573+
"refresh_token": refreshToken,
574+
]
575+
576+
return try await provider.request(
577+
with: .post,
578+
endpoint: .loginToken,
579+
form: form,
580+
authorization: false
581+
).data()
582+
}
583+
567584
/// Register device and retrieve device ID for push notifications
568585
/// - Parameter stamp: Authorization stamp for device registration
569586
/// - Returns: Unique device ID for this installation

KiaMaps/Core/Api/ApiRequest.swift

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ protocol ApiRequest {
132132
/// - headers: HTTP headers
133133
/// - encodable: Codable object to encode as JSON body
134134
/// - timeout: Request timeout in seconds
135+
/// - authorization: If we should set authorization header
135136
/// - Throws: Encoding errors if encodable cannot be serialized
136137
init(
137138
caller: ApiCaller,
@@ -140,7 +141,8 @@ protocol ApiRequest {
140141
queryItems: [URLQueryItem],
141142
headers: Headers,
142143
encodable: Encodable,
143-
timeout: TimeInterval
144+
timeout: TimeInterval,
145+
authorization: Bool
144146
) throws
145147

146148
/// Initializer for requests with raw Data body
@@ -152,14 +154,16 @@ protocol ApiRequest {
152154
/// - headers: HTTP headers
153155
/// - body: Raw data for request body
154156
/// - timeout: Request timeout in seconds
157+
/// - authorization: If we should set authorization header
155158
init(
156159
caller: ApiCaller,
157160
method: ApiMethod?,
158161
endpoint: ApiEndpoint,
159162
queryItems: [URLQueryItem],
160163
headers: Headers,
161164
body: Data?,
162-
timeout: TimeInterval
165+
timeout: TimeInterval,
166+
authorization: Bool
163167
)
164168

165169
/// Initializer for form-encoded requests
@@ -171,14 +175,16 @@ protocol ApiRequest {
171175
/// - headers: HTTP headers
172176
/// - form: Form data dictionary
173177
/// - timeout: Request timeout in seconds
178+
/// - authorization: If we should set authorization header
174179
init(
175180
caller: ApiCaller,
176181
method: ApiMethod?,
177182
endpoint: ApiEndpoint,
178183
queryItems: [URLQueryItem],
179184
headers: Headers,
180185
form: Form,
181-
timeout: TimeInterval
186+
timeout: TimeInterval,
187+
authorization: Bool
182188
)
183189

184190
/// The configured URLRequest ready for execution
@@ -291,6 +297,8 @@ struct ApiRequestImpl: ApiRequest {
291297
let body: Data?
292298
/// Request timeout in seconds
293299
let timeout: TimeInterval
300+
/// If we should set authorization header
301+
let authorization: Bool
294302

295303
/// Character set used for form data encoding
296304
private static let formCharset: CharacterSet = {
@@ -310,7 +318,8 @@ struct ApiRequestImpl: ApiRequest {
310318
queryItems: [URLQueryItem],
311319
headers: Headers,
312320
encodable: Encodable,
313-
timeout: TimeInterval
321+
timeout: TimeInterval,
322+
authorization: Bool
314323
) throws {
315324
var headers = headers
316325
if headers["Content-type"] == nil {
@@ -326,6 +335,7 @@ struct ApiRequestImpl: ApiRequest {
326335
self.headers = headers
327336
body = try JSONEncoders.default.encode(encodable)
328337
self.timeout = timeout
338+
self.authorization = authorization
329339
}
330340

331341
init(
@@ -335,7 +345,8 @@ struct ApiRequestImpl: ApiRequest {
335345
queryItems: [URLQueryItem],
336346
headers: Headers,
337347
body: Data?,
338-
timeout: TimeInterval
348+
timeout: TimeInterval,
349+
authorization: Bool
339350
) {
340351
var headers = headers
341352
if headers["Content-type"] == nil {
@@ -351,16 +362,17 @@ struct ApiRequestImpl: ApiRequest {
351362
self.headers = headers
352363
self.body = body
353364
self.timeout = timeout
365+
self.authorization = authorization
354366
}
355367

356-
init(
357-
caller: ApiCaller,
358-
method: ApiMethod?,
359-
endpoint: ApiEndpoint,
360-
queryItems: [URLQueryItem],
361-
headers: Headers,
362-
form: Form,
363-
timeout: TimeInterval
368+
init(caller: ApiCaller,
369+
method: ApiMethod?,
370+
endpoint: ApiEndpoint,
371+
queryItems: [URLQueryItem],
372+
headers: Headers,
373+
form: Form,
374+
timeout: TimeInterval,
375+
authorization: Bool
364376
) {
365377
var headers = Self.commonFormHeaders
366378
headers["User-Agent"] = caller.configuration.userAgent
@@ -378,6 +390,7 @@ struct ApiRequestImpl: ApiRequest {
378390
self.headers = headers
379391
body = formData
380392
self.timeout = timeout
393+
self.authorization = authorization
381394
}
382395

383396
var urlRequest: URLRequest {
@@ -389,7 +402,7 @@ struct ApiRequestImpl: ApiRequest {
389402
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: timeout)
390403
request.httpMethod = method.rawValue
391404
var headers = self.headers
392-
if let authorization = caller.authorization {
405+
if let authorization = caller.authorization, self.authorization {
393406
for (key, value) in authorization.authorizatioHeaders(for: caller.configuration) {
394407
headers[key] = value
395408
}
@@ -550,6 +563,7 @@ class ApiRequestProvider: NSObject {
550563
/// - headers: HTTP headers
551564
/// - encodable: Object to encode as JSON body
552565
/// - timeout: Request timeout
566+
/// - authorization: If authorization header should be set
553567
/// - Returns: Configured API request
554568
/// - Throws: Encoding errors
555569
func request(
@@ -558,7 +572,8 @@ class ApiRequestProvider: NSObject {
558572
queryItems: [URLQueryItem] = [],
559573
headers: ApiRequest.Headers = [:],
560574
encodable: Encodable,
561-
timeout: TimeInterval = ApiDefaultTimeout
575+
timeout: TimeInterval = ApiDefaultTimeout,
576+
authorization: Bool = true
562577
) throws -> ApiRequest {
563578
try requestType.init(
564579
caller: caller,
@@ -567,7 +582,8 @@ class ApiRequestProvider: NSObject {
567582
queryItems: queryItems,
568583
headers: headers,
569584
encodable: encodable,
570-
timeout: timeout
585+
timeout: timeout,
586+
authorization: authorization
571587
)
572588
}
573589

@@ -579,14 +595,16 @@ class ApiRequestProvider: NSObject {
579595
/// - headers: HTTP headers
580596
/// - body: Raw body data
581597
/// - timeout: Request timeout
598+
/// - authorization: If authorization header should be set
582599
/// - Returns: Configured API request
583600
func request(
584601
with method: ApiMethod? = nil,
585602
endpoint: ApiEndpoint,
586603
queryItems: [URLQueryItem] = [],
587604
headers: ApiRequest.Headers = [:],
588605
body: Data? = nil,
589-
timeout: TimeInterval = ApiDefaultTimeout
606+
timeout: TimeInterval = ApiDefaultTimeout,
607+
authorization: Bool = true
590608
) -> ApiRequest {
591609
requestType.init(
592610
caller: caller,
@@ -595,7 +613,8 @@ class ApiRequestProvider: NSObject {
595613
queryItems: queryItems,
596614
headers: headers,
597615
body: body,
598-
timeout: timeout
616+
timeout: timeout,
617+
authorization: authorization
599618
)
600619
}
601620

@@ -607,14 +626,16 @@ class ApiRequestProvider: NSObject {
607626
/// - headers: HTTP headers
608627
/// - string: String to encode as UTF-8 body
609628
/// - timeout: Request timeout
629+
/// - authorization: If authorization header should be set
610630
/// - Returns: Configured API request
611631
func request(
612632
with method: ApiMethod? = nil,
613633
endpoint: ApiEndpoint,
614634
queryItems: [URLQueryItem] = [],
615635
headers: ApiRequest.Headers = [:],
616636
string: String,
617-
timeout: TimeInterval = ApiDefaultTimeout
637+
timeout: TimeInterval = ApiDefaultTimeout,
638+
authorization: Bool = true
618639
) -> ApiRequest {
619640
requestType.init(
620641
caller: caller,
@@ -623,7 +644,8 @@ class ApiRequestProvider: NSObject {
623644
queryItems: queryItems,
624645
headers: headers,
625646
body: string.data(using: .utf8),
626-
timeout: timeout
647+
timeout: timeout,
648+
authorization: authorization
627649
)
628650
}
629651

@@ -635,14 +657,16 @@ class ApiRequestProvider: NSObject {
635657
/// - headers: HTTP headers
636658
/// - form: Form data dictionary
637659
/// - timeout: Request timeout
660+
/// - authorization: If authorization header should be set
638661
/// - Returns: Configured API request
639662
func request(
640663
with method: ApiMethod? = nil,
641664
endpoint: ApiEndpoint,
642665
queryItems: [URLQueryItem] = [],
643666
headers: ApiRequest.Headers = [:],
644667
form: ApiRequest.Form,
645-
timeout: TimeInterval = ApiDefaultTimeout
668+
timeout: TimeInterval = ApiDefaultTimeout,
669+
authorization: Bool = true
646670
) -> ApiRequest {
647671
requestType.init(
648672
caller: caller,
@@ -651,7 +675,8 @@ class ApiRequestProvider: NSObject {
651675
queryItems: queryItems,
652676
headers: headers,
653677
form: form,
654-
timeout: timeout
678+
timeout: timeout,
679+
authorization: authorization
655680
)
656681
}
657682

KiaMaps/Core/Api/Models/AuthenticationModels.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ struct TokenResponse: Codable {
9797
let scope: String?
9898
let connector: [String: ConnectorTokenInfo]?
9999
let accessToken: String
100-
let refreshToken: String
100+
let refreshToken: String?
101101
let idToken: String?
102102
let tokenType: String
103103
let expiresIn: Int

0 commit comments

Comments
 (0)