Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions KiaMaps/App/ApiExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,52 @@ extension Api {
}
}

/// Attempts to refresh the token using stored credentials
/// Attempts to refresh the token using stored refresh token or fallback to credentials
/// - returns: True if token was successfully refreshed, false otherwise
private func refreshTokenIfPossible() async -> Bool {
// Check if we have stored login credentials
guard let storedCredentials = LoginCredentialManager.retrieveCredentials() else {
logInfo("No stored credentials available for token refresh", category: .auth)
return false
}

// Check if current token is actually expired before attempting refresh
if let currentAuth = authorization, !isTokenExpired(currentAuth) {
logDebug("Current token is not expired, no refresh needed", category: .auth)
return true
}

// First, try to refresh using the refresh token if available
if let currentAuth = authorization {
logInfo("Attempting to refresh token using refresh token", category: .auth)

do {
let tokenResponse = try await refreshToken(currentAuth.refreshToken)

// Create new authorization data with refreshed tokens
let newAuthData = AuthorizationData(
stamp: currentAuth.stamp, // Keep existing stamp
deviceId: currentAuth.deviceId, // Keep existing device ID
accessToken: tokenResponse.accessToken,
expiresIn: tokenResponse.expiresIn,
refreshToken: tokenResponse.refreshToken ?? currentAuth.refreshToken,
isCcuCCS2Supported: currentAuth.isCcuCCS2Supported
)

// Store the new authorization data
Authorization.store(data: newAuthData)
self.authorization = newAuthData

logInfo("Successfully refreshed token using refresh token", category: .auth)
return true
} catch {
logWarning("Refresh token failed: \(error.localizedDescription). Falling back to credential login", category: .auth)
// Continue to fallback method below
}
}

// Fallback: Try to refresh using stored login credentials
guard let storedCredentials = LoginCredentialManager.retrieveCredentials() else {
logInfo("No stored credentials available for fallback token refresh", category: .auth)
return false
}

do {
logInfo("Attempting to login with stored credentials", category: .auth)
logInfo("Attempting fallback login with stored credentials", category: .auth)
let newAuthData = try await login(
username: storedCredentials.username,
password: storedCredentials.password
Expand All @@ -67,11 +96,11 @@ extension Api {
Authorization.store(data: newAuthData)
self.authorization = newAuthData

logInfo("Successfully refreshed token", category: .auth)
logInfo("Successfully refreshed token using credential fallback", category: .auth)
return true

} catch {
logError("Failed to refresh token with error: \(error.localizedDescription)", category: .auth)
logError("Failed to refresh token with fallback login: \(error.localizedDescription)", category: .auth)

// If login fails, clear the stored credentials as they might be invalid
if error is ApiError {
Expand Down
19 changes: 18 additions & 1 deletion KiaMaps/Core/Api/Api.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class Api {
deviceId: deviceId,
accessToken: tokenResponse.accessToken,
expiresIn: tokenResponse.expiresIn,
refreshToken: tokenResponse.refreshToken,
refreshToken: tokenResponse.refreshToken ?? "",
isCcuCCS2Supported: true
)

Expand Down Expand Up @@ -564,6 +564,23 @@ extension Api {
).data()
}

/// Login - Refresh token
func refreshToken(_ refreshToken: String) async throws -> TokenResponse {
let form: [String: String] = [
"client_id": configuration.serviceId,
"client_secret": "secret", // TODO: something generated
"grant_type": "refresh_token",
"refresh_token": refreshToken,
]

return try await provider.request(
with: .post,
endpoint: .loginToken,
form: form,
authorization: false
).data()
}

/// Register device and retrieve device ID for push notifications
/// - Parameter stamp: Authorization stamp for device registration
/// - Returns: Unique device ID for this installation
Expand Down
69 changes: 47 additions & 22 deletions KiaMaps/Core/Api/ApiRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ protocol ApiRequest {
/// - headers: HTTP headers
/// - encodable: Codable object to encode as JSON body
/// - timeout: Request timeout in seconds
/// - authorization: If we should set authorization header
/// - Throws: Encoding errors if encodable cannot be serialized
init(
caller: ApiCaller,
Expand All @@ -140,7 +141,8 @@ protocol ApiRequest {
queryItems: [URLQueryItem],
headers: Headers,
encodable: Encodable,
timeout: TimeInterval
timeout: TimeInterval,
authorization: Bool
) throws

/// Initializer for requests with raw Data body
Expand All @@ -152,14 +154,16 @@ protocol ApiRequest {
/// - headers: HTTP headers
/// - body: Raw data for request body
/// - timeout: Request timeout in seconds
/// - authorization: If we should set authorization header
init(
caller: ApiCaller,
method: ApiMethod?,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem],
headers: Headers,
body: Data?,
timeout: TimeInterval
timeout: TimeInterval,
authorization: Bool
)

/// Initializer for form-encoded requests
Expand All @@ -171,14 +175,16 @@ protocol ApiRequest {
/// - headers: HTTP headers
/// - form: Form data dictionary
/// - timeout: Request timeout in seconds
/// - authorization: If we should set authorization header
init(
caller: ApiCaller,
method: ApiMethod?,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem],
headers: Headers,
form: Form,
timeout: TimeInterval
timeout: TimeInterval,
authorization: Bool
)

/// The configured URLRequest ready for execution
Expand Down Expand Up @@ -291,6 +297,8 @@ struct ApiRequestImpl: ApiRequest {
let body: Data?
/// Request timeout in seconds
let timeout: TimeInterval
/// If we should set authorization header
let authorization: Bool

/// Character set used for form data encoding
private static let formCharset: CharacterSet = {
Expand All @@ -310,7 +318,8 @@ struct ApiRequestImpl: ApiRequest {
queryItems: [URLQueryItem],
headers: Headers,
encodable: Encodable,
timeout: TimeInterval
timeout: TimeInterval,
authorization: Bool
) throws {
var headers = headers
if headers["Content-type"] == nil {
Expand All @@ -326,6 +335,7 @@ struct ApiRequestImpl: ApiRequest {
self.headers = headers
body = try JSONEncoders.default.encode(encodable)
self.timeout = timeout
self.authorization = authorization
}

init(
Expand All @@ -335,7 +345,8 @@ struct ApiRequestImpl: ApiRequest {
queryItems: [URLQueryItem],
headers: Headers,
body: Data?,
timeout: TimeInterval
timeout: TimeInterval,
authorization: Bool
) {
var headers = headers
if headers["Content-type"] == nil {
Expand All @@ -351,16 +362,17 @@ struct ApiRequestImpl: ApiRequest {
self.headers = headers
self.body = body
self.timeout = timeout
self.authorization = authorization
}

init(
caller: ApiCaller,
method: ApiMethod?,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem],
headers: Headers,
form: Form,
timeout: TimeInterval
init(caller: ApiCaller,
method: ApiMethod?,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem],
headers: Headers,
form: Form,
timeout: TimeInterval,
authorization: Bool
) {
var headers = Self.commonFormHeaders
headers["User-Agent"] = caller.configuration.userAgent
Expand All @@ -378,6 +390,7 @@ struct ApiRequestImpl: ApiRequest {
self.headers = headers
body = formData
self.timeout = timeout
self.authorization = authorization
}

var urlRequest: URLRequest {
Expand All @@ -389,7 +402,7 @@ struct ApiRequestImpl: ApiRequest {
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: timeout)
request.httpMethod = method.rawValue
var headers = self.headers
if let authorization = caller.authorization {
if let authorization = caller.authorization, self.authorization {
for (key, value) in authorization.authorizatioHeaders(for: caller.configuration) {
headers[key] = value
}
Expand Down Expand Up @@ -550,6 +563,7 @@ class ApiRequestProvider: NSObject {
/// - headers: HTTP headers
/// - encodable: Object to encode as JSON body
/// - timeout: Request timeout
/// - authorization: If authorization header should be set
/// - Returns: Configured API request
/// - Throws: Encoding errors
func request(
Expand All @@ -558,7 +572,8 @@ class ApiRequestProvider: NSObject {
queryItems: [URLQueryItem] = [],
headers: ApiRequest.Headers = [:],
encodable: Encodable,
timeout: TimeInterval = ApiDefaultTimeout
timeout: TimeInterval = ApiDefaultTimeout,
authorization: Bool = true
) throws -> ApiRequest {
try requestType.init(
caller: caller,
Expand All @@ -567,7 +582,8 @@ class ApiRequestProvider: NSObject {
queryItems: queryItems,
headers: headers,
encodable: encodable,
timeout: timeout
timeout: timeout,
authorization: authorization
)
}

Expand All @@ -579,14 +595,16 @@ class ApiRequestProvider: NSObject {
/// - headers: HTTP headers
/// - body: Raw body data
/// - timeout: Request timeout
/// - authorization: If authorization header should be set
/// - Returns: Configured API request
func request(
with method: ApiMethod? = nil,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem] = [],
headers: ApiRequest.Headers = [:],
body: Data? = nil,
timeout: TimeInterval = ApiDefaultTimeout
timeout: TimeInterval = ApiDefaultTimeout,
authorization: Bool = true
) -> ApiRequest {
requestType.init(
caller: caller,
Expand All @@ -595,7 +613,8 @@ class ApiRequestProvider: NSObject {
queryItems: queryItems,
headers: headers,
body: body,
timeout: timeout
timeout: timeout,
authorization: authorization
)
}

Expand All @@ -607,14 +626,16 @@ class ApiRequestProvider: NSObject {
/// - headers: HTTP headers
/// - string: String to encode as UTF-8 body
/// - timeout: Request timeout
/// - authorization: If authorization header should be set
/// - Returns: Configured API request
func request(
with method: ApiMethod? = nil,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem] = [],
headers: ApiRequest.Headers = [:],
string: String,
timeout: TimeInterval = ApiDefaultTimeout
timeout: TimeInterval = ApiDefaultTimeout,
authorization: Bool = true
) -> ApiRequest {
requestType.init(
caller: caller,
Expand All @@ -623,7 +644,8 @@ class ApiRequestProvider: NSObject {
queryItems: queryItems,
headers: headers,
body: string.data(using: .utf8),
timeout: timeout
timeout: timeout,
authorization: authorization
)
}

Expand All @@ -635,14 +657,16 @@ class ApiRequestProvider: NSObject {
/// - headers: HTTP headers
/// - form: Form data dictionary
/// - timeout: Request timeout
/// - authorization: If authorization header should be set
/// - Returns: Configured API request
func request(
with method: ApiMethod? = nil,
endpoint: ApiEndpoint,
queryItems: [URLQueryItem] = [],
headers: ApiRequest.Headers = [:],
form: ApiRequest.Form,
timeout: TimeInterval = ApiDefaultTimeout
timeout: TimeInterval = ApiDefaultTimeout,
authorization: Bool = true
) -> ApiRequest {
requestType.init(
caller: caller,
Expand All @@ -651,7 +675,8 @@ class ApiRequestProvider: NSObject {
queryItems: queryItems,
headers: headers,
form: form,
timeout: timeout
timeout: timeout,
authorization: authorization
)
}

Expand Down
2 changes: 1 addition & 1 deletion KiaMaps/Core/Api/Models/AuthenticationModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ struct TokenResponse: Codable {
let scope: String?
let connector: [String: ConnectorTokenInfo]?
let accessToken: String
let refreshToken: String
let refreshToken: String?
let idToken: String?
let tokenType: String
let expiresIn: Int
Expand Down
Loading