From 03113a229f01d539dd580421e03dd19c48ffa5f5 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 9 Jan 2024 12:54:59 +0100 Subject: [PATCH 1/5] refactor: Remove applicationPassword getApiToken --- .../Requests/InfomaniakNetworkLogin.swift | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift index 31e77d6..2edd658 100644 --- a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift +++ b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift @@ -36,9 +36,6 @@ public protocol InfomaniakNetworkLoginable { /// Get an api token async (callback on background thread) func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) - /// Get an api token async from an application password (callback on background thread) - func getApiToken(username: String, applicationPassword: String, completion: @escaping (ApiToken?, Error?) -> Void) - /// Refresh api token async (callback on background thread) func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) @@ -81,23 +78,6 @@ public class InfomaniakNetworkLogin: InfomaniakNetworkLoginable { getApiToken(request: request, completion: completion) } - public func getApiToken(username: String, applicationPassword: String, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "password", - "access_type": "offline", - "client_id": clientId, - "username": username, - "password": applicationPassword - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - public func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) { var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) From 9a7940c9e4b47c306784e131d1159f1d91c13ec6 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 10 Jan 2024 08:11:45 +0100 Subject: [PATCH 2/5] refactor: Remove InfomaniakNetworkLogin from core --- Package.resolved | 9 + Package.swift | 2 + Sources/InfomaniakCore/Account/Account.swift | 1 + .../Account/KeychainHelper.swift | 63 ++++--- .../Networking/ApiFetcher.swift | 9 +- .../InfomaniakCore/Networking/ApiToken.swift | 91 --------- .../Requests/InfomaniakNetworkLogin.swift | 174 ------------------ 7 files changed, 56 insertions(+), 293 deletions(-) delete mode 100644 Sources/InfomaniakCore/Networking/ApiToken.swift delete mode 100644 Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift diff --git a/Package.resolved b/Package.resolved index b5e229c..8988ebb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "2.0.1" } }, + { + "identity" : "ios-login", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Infomaniak/ios-login", + "state" : { + "branch" : "login-refactor", + "revision" : "3c48ead189a5dc0602f53e32666f32d026599200" + } + }, { "identity" : "realm-core", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 5fd0dc6..8d81176 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Infomaniak/ios-login", branch: "login-refactor"), .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")), .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.0")), @@ -29,6 +30,7 @@ let package = Package( dependencies: [ "Alamofire", .product(name: "InfomaniakDI", package: "ios-dependency-injection"), + .product(name: "InfomaniakLogin", package: "ios-login"), .product(name: "Sentry", package: "sentry-cocoa"), .product(name: "RealmSwift", package: "realm-swift"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), diff --git a/Sources/InfomaniakCore/Account/Account.swift b/Sources/InfomaniakCore/Account/Account.swift index 6617e74..81ad1b1 100644 --- a/Sources/InfomaniakCore/Account/Account.swift +++ b/Sources/InfomaniakCore/Account/Account.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakLogin public protocol AccountUpdateDelegate { func didUpdateCurrentAccount(_ account: Account) diff --git a/Sources/InfomaniakCore/Account/KeychainHelper.swift b/Sources/InfomaniakCore/Account/KeychainHelper.swift index ad067cf..ac434db 100644 --- a/Sources/InfomaniakCore/Account/KeychainHelper.swift +++ b/Sources/InfomaniakCore/Account/KeychainHelper.swift @@ -1,43 +1,44 @@ /* Infomaniak Core - iOS Copyright (C) 2023 Infomaniak Network SA - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program. If not, see . */ import CocoaLumberjackSwift import Foundation +import InfomaniakLogin import Sentry public class KeychainHelper { let accessGroup: String let tag = "ch.infomaniak.token".data(using: .utf8)! let keychainQueue = DispatchQueue(label: "com.infomaniak.keychain") - + let lockedKey = "isLockedKey" let lockedValue = "locked".data(using: .utf8)! var accessibilityValueWritten = false - + public init(accessGroup: String) { self.accessGroup = accessGroup } - + public var isKeychainAccessible: Bool { if !accessibilityValueWritten { initKeychainAccessibility() } - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: lockedKey, @@ -47,13 +48,13 @@ public class KeychainHelper { kSecReturnRef as String: kCFBooleanTrue as Any, kSecMatchLimit as String: kSecMatchLimitAll ] - + var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) } - + if resultCode == noErr, let array = result as? [[String: Any]] { for item in array { if let value = item[kSecValueData as String] as? Data { @@ -66,7 +67,7 @@ public class KeychainHelper { return false } } - + func initKeychainAccessibility() { accessibilityValueWritten = true let queryAdd: [String: Any] = [ @@ -81,7 +82,7 @@ public class KeychainHelper { "[Keychain] Successfully init KeychainHelper ? \(resultCode == noErr || resultCode == errSecDuplicateItem), \(resultCode)" ) } - + public func deleteToken(for userId: Int) { keychainQueue.sync { let queryDelete: [String: Any] = [ @@ -93,7 +94,7 @@ public class KeychainHelper { DDLogInfo("Successfully deleted token ? \(resultCode == noErr)") } } - + public func deleteAllTokens() { keychainQueue.sync { let queryDelete: [String: Any] = [ @@ -104,12 +105,12 @@ public class KeychainHelper { DDLogInfo("Successfully deleted all tokens ? \(resultCode == noErr)") } } - + public func storeToken(_ token: ApiToken) { var resultCode: OSStatus = noErr let tokenData = try! JSONEncoder().encode(token) - + if let savedToken = getSavedToken(for: token.userId) { keychainQueue.sync { // Save token only if it's more recent @@ -118,7 +119,7 @@ public class KeychainHelper { kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "\(token.userId)" ] - + let attributes: [String: Any] = [ kSecValueData as String: tokenData ] @@ -149,7 +150,7 @@ public class KeychainHelper { .generateBreadcrumb(level: .error, message: "Failed saving token", keychainError: resultCode)) } } - + public func getSavedToken(for userId: Int) -> ApiToken? { var savedToken: ApiToken? keychainQueue.sync { @@ -164,11 +165,11 @@ public class KeychainHelper { kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(queryFindOne as CFDictionary, UnsafeMutablePointer($0)) } - + let jsonDecoder = JSONDecoder() if resultCode == noErr, let keychainItem = result as? [String: Any], @@ -179,7 +180,7 @@ public class KeychainHelper { } return savedToken } - + public func loadTokens() -> [ApiToken] { var values = [ApiToken]() keychainQueue.sync { @@ -192,14 +193,14 @@ public class KeychainHelper { kSecReturnRef as String: kCFBooleanTrue as Any, kSecMatchLimit as String: kSecMatchLimitAll ] - + var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) } DDLogInfo("Successfully loaded tokens ? \(resultCode == noErr)") - + guard resultCode == noErr else { let crumb = Breadcrumb(level: .error, category: "Token") crumb.type = "error" @@ -208,7 +209,7 @@ public class KeychainHelper { SentrySDK.addBreadcrumb(crumb) return } - + if let array = result as? [[String: Any]] { let jsonDecoder = JSONDecoder() for item in array { @@ -226,3 +227,17 @@ public class KeychainHelper { return values } } + +public extension ApiToken { + func generateBreadcrumb(level: SentryLevel, message: String, keychainError: OSStatus = noErr) -> Breadcrumb { + let crumb = Breadcrumb(level: level, category: "Token") + crumb.type = level == .info ? "info" : "error" + crumb.message = message + crumb.data = ["User id": userId, + "Expiration date": expirationDate.timeIntervalSince1970, + "Access Token": truncatedAccessToken, + "Refresh Token": truncatedRefreshToken, + "Keychain error code": keychainError] + return crumb + } +} diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 6986538..824e53f 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -19,6 +19,7 @@ import Alamofire import Foundation import InfomaniakDI +import InfomaniakLogin import Sentry public protocol RefreshTokenDelegate: AnyObject { @@ -29,14 +30,14 @@ public protocol RefreshTokenDelegate: AnyObject { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) open class ApiFetcher { public typealias RequestModifier = (inout URLRequest) throws -> Void - + /// All status except 401 are handled by our code, 401 status is handled by Alamofire's Authenticator code private static var handledHttpStatus: Set = { var allStatus = Set(200 ... 500) allStatus.remove(401) return allStatus }() - + public var authenticatedSession: Session! public static var decoder: JSONDecoder = { let decoder = JSONDecoder() @@ -139,8 +140,8 @@ open class ApiFetcher { decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) { let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus) let response = await validatedRequest.serializingDecodable(ApiResponse.self, - automaticallyCancelling: true, - decoder: decoder).response + automaticallyCancelling: true, + decoder: decoder).response let apiResponse = try response.result.get() return try handleApiResponse(apiResponse, responseStatusCode: response.response?.statusCode ?? -1) } diff --git a/Sources/InfomaniakCore/Networking/ApiToken.swift b/Sources/InfomaniakCore/Networking/ApiToken.swift deleted file mode 100644 index 71ba410..0000000 --- a/Sources/InfomaniakCore/Networking/ApiToken.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - Infomaniak Core - iOS - Copyright (C) 2023 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import Sentry - -@objc public class ApiToken: NSObject, Codable { - @objc public var accessToken: String - @objc public var expiresIn: Int - @objc public var refreshToken: String - @objc public var scope: String - @objc public var tokenType: String - @objc public var userId: Int - @objc public var expirationDate: Date - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - case userId = "user_id" - case scope - case expirationDate - } - - public required init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - accessToken = try values.decode(String.self, forKey: .accessToken) - expiresIn = try values.decode(Int.self, forKey: .expiresIn) - refreshToken = try values.decode(String.self, forKey: .refreshToken) - scope = try values.decode(String.self, forKey: .scope) - tokenType = try values.decode(String.self, forKey: .tokenType) - userId = try values.decode(Int.self, forKey: .userId) - - let newExpirationDate = Date().addingTimeInterval(TimeInterval(Double(expiresIn))) - expirationDate = try values.decodeIfPresent(Date.self, forKey: .expirationDate) ?? newExpirationDate - } - - public init(accessToken: String, expiresIn: Int, refreshToken: String, scope: String, tokenType: String, userId: Int, expirationDate: Date) { - self.accessToken = accessToken - self.expiresIn = expiresIn - self.refreshToken = refreshToken - self.scope = scope - self.tokenType = tokenType - self.userId = userId - self.expirationDate = expirationDate - } -} - -// MARK: - Token Logging - -extension ApiToken { - public var truncatedAccessToken: String { - truncateToken(accessToken) - } - - public var truncatedRefreshToken: String { - truncateToken(refreshToken) - } - - func truncateToken(_ token: String) -> String { - String(token.prefix(4) + "-*****-" + token.suffix(4)) - } - - public func generateBreadcrumb(level: SentryLevel, message: String, keychainError: OSStatus = noErr) -> Breadcrumb { - let crumb = Breadcrumb(level: level, category: "Token") - crumb.type = level == .info ? "info" : "error" - crumb.message = message - crumb.data = ["User id": userId, - "Expiration date": expirationDate.timeIntervalSince1970, - "Access Token": truncatedAccessToken, - "Refresh Token": truncatedRefreshToken, - "Keychain error code": keychainError] - return crumb - } -} diff --git a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift deleted file mode 100644 index 2edd658..0000000 --- a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - Infomaniak Core - iOS - Copyright (C) 2023 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation - -public enum Constants { - public static let LOGIN_URL = "https://login.infomaniak.com/" - public static let DELETEACCOUNT_URL = "https://manager.infomaniak.com/v3/ng/profile/user/dashboard?open-terminate-account-modal" - public static let RESPONSE_TYPE = "code" - public static let ACCESS_TYPE = "offline" - public static let HASH_MODE = "SHA-256" - public static let HASH_MODE_SHORT = "S256" - - public static func autologinUrl(to destination: String) -> URL? { - return URL(string: "https://manager.infomaniak.com/v3/mobile_login/?url=\(destination)") - } -} - -/// Something that can keep the network stack authenticated -public protocol InfomaniakNetworkLoginable { - /// Get an api token async (callback on background thread) - func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) - - /// Refresh api token async (callback on background thread) - func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) - - /// Delete an api token async - func deleteApiToken(token: ApiToken, onError: @escaping (Error) -> Void) -} - -public class InfomaniakNetworkLogin: InfomaniakNetworkLoginable { - private static let LOGIN_API_URL = "https://login.infomaniak.com/" - private static let GET_TOKEN_API_URL = LOGIN_API_URL + "token" - - private var clientId: String - private var loginBaseUrl: String - private var redirectUri: String - - // MARK: Public - - public init(clientId: String, - loginUrl: String = Constants.LOGIN_URL, - redirectUri: String = "\(Bundle.main.bundleIdentifier ?? "")://oauth2redirect") { - self.loginBaseUrl = loginUrl - self.clientId = clientId - self.redirectUri = redirectUri - } - - public func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "authorization_code", - "client_id": clientId, - "code": code, - "code_verifier": codeVerifier, - "redirect_uri": redirectUri - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - - public func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "refresh_token", - "client_id": clientId, - "refresh_token": token.refreshToken - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - - public func deleteApiToken(token: ApiToken, onError: @escaping (Error) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - request.addValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization") - request.httpMethod = "DELETE" - - URLSession.shared.dataTask(with: request) { data, response, sessionError in - guard let response = response as? HTTPURLResponse, let data else { - if let sessionError { - onError(sessionError) - } - return - } - - do { - if !response.isSuccessful() { - let apiDeleteToken = try JSONDecoder().decode(ApiDeleteToken.self, from: data) - onError(NSError(domain: apiDeleteToken.error!, code: response.statusCode, userInfo: ["Error": apiDeleteToken.error!])) - } - } catch { - onError(error) - } - }.resume() - } - - // MARK: Private - - /// Make the get token network call - private func getApiToken(request: URLRequest, completion: @escaping (ApiToken?, Error?) -> Void) { - let session = URLSession.shared - session.dataTask(with: request) { data, response, sessionError in - guard let response = response as? HTTPURLResponse, - let data = data, data.count > 0 else { - completion(nil, sessionError) - return - } - - do { - if response.isSuccessful() { - let apiToken = try JSONDecoder().decode(ApiToken.self, from: data) - completion(apiToken, nil) - } else { - let apiError = try JSONDecoder().decode(LoginApiError.self, from: data) - completion(nil, NSError(domain: apiError.error, code: response.statusCode, userInfo: ["Error": apiError])) - } - } catch { - completion(nil, error) - } - }.resume() - } -} - -extension HTTPURLResponse { - func isSuccessful() -> Bool { - return statusCode >= 200 && statusCode <= 299 - } -} - -extension Dictionary { - func percentEncoded() -> Data? { - return map { key, value in - let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" - let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" - return escapedKey + "=" + escapedValue - } - .joined(separator: "&") - .data(using: .utf8) - } -} - -extension CharacterSet { - static let urlQueryValueAllowed: CharacterSet = { - let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 - let subDelimitersToEncode = "!$&'()*+,;=" - - var allowed = CharacterSet.urlQueryAllowed - allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") - return allowed - }() -} From 295a7494ce939d82bc741017fe435a1f9f6d56ea Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 15 Jan 2024 13:18:11 +0100 Subject: [PATCH 3/5] chore: Upgrade dependencies --- Package.resolved | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8988ebb..5a671a9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -12,7 +12,7 @@ { "identity" : "cocoalumberjack", "kind" : "remoteSourceControl", - "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack", "state" : { "revision" : "363ed23d19a931809ea834a7d722da830353806a", "version" : "3.8.2" @@ -33,7 +33,7 @@ "location" : "https://github.com/Infomaniak/ios-login", "state" : { "branch" : "login-refactor", - "revision" : "3c48ead189a5dc0602f53e32666f32d026599200" + "revision" : "9e34ff1ab1df7f8c841f368e8982e0024883579c" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { From 52a17e290f887bfcb518c330beb58ff3dab87daa Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 16 Jan 2024 13:47:46 +0100 Subject: [PATCH 4/5] chore: Update core references --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5a671a9..cc7ad8c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-login", "state" : { - "branch" : "login-refactor", - "revision" : "9e34ff1ab1df7f8c841f368e8982e0024883579c" + "revision" : "904c1ac39b4db56212302b464a0b2e023d9b5791", + "version" : "6.0.0" } }, { diff --git a/Package.swift b/Package.swift index 8d81176..1ac21f0 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")), - .package(url: "https://github.com/Infomaniak/ios-login", branch: "login-refactor"), + .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "6.0.0")), .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")), .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.0")), From dc0c5266eb082d8da0bd6487cacfb0d8cb137131 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 16 Jan 2024 14:06:21 +0100 Subject: [PATCH 5/5] fix: Accomodate infinite refresh token --- .../Account/KeychainHelper.swift | 31 +++++++++++++------ .../Networking/ApiFetcher.swift | 3 ++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/InfomaniakCore/Account/KeychainHelper.swift b/Sources/InfomaniakCore/Account/KeychainHelper.swift index ac434db..7eb3974 100644 --- a/Sources/InfomaniakCore/Account/KeychainHelper.swift +++ b/Sources/InfomaniakCore/Account/KeychainHelper.swift @@ -113,19 +113,30 @@ public class KeychainHelper { if let savedToken = getSavedToken(for: token.userId) { keychainQueue.sync { + let queryUpdate: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "\(token.userId)" + ] + + let attributes: [String: Any] = [ + kSecValueData as String: tokenData + ] + // Save token only if it's more recent - if savedToken.expirationDate <= token.expirationDate { - let queryUpdate: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: "\(token.userId)" - ] - - let attributes: [String: Any] = [ - kSecValueData as String: tokenData - ] + if let savedTokenExpirationDate = savedToken.expirationDate, + let newTokenExpirationDate = token.expirationDate, + savedTokenExpirationDate <= newTokenExpirationDate { resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary) DDLogInfo("Successfully updated token ? \(resultCode == noErr)") SentrySDK.addBreadcrumb(token.generateBreadcrumb(level: .info, message: "Successfully updated token")) + } else if savedToken.expirationDate == nil || token.expirationDate == nil { + // Or if one of them is now an infinite refresh token + resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary) + DDLogInfo("Successfully updated unlimited token ? \(resultCode == noErr)") + SentrySDK.addBreadcrumb(token.generateBreadcrumb( + level: .info, + message: "Successfully updated unlimited token" + )) } } } else { @@ -234,7 +245,7 @@ public extension ApiToken { crumb.type = level == .info ? "info" : "error" crumb.message = message crumb.data = ["User id": userId, - "Expiration date": expirationDate.timeIntervalSince1970, + "Expiration date": expirationDate?.timeIntervalSince1970 ?? "infinite", "Access Token": truncatedAccessToken, "Refresh Token": truncatedRefreshToken, "Keychain error code": keychainError] diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 824e53f..48b46a1 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -215,6 +215,9 @@ open class OAuthAuthenticator: Authenticator { extension ApiToken: AuthenticationCredential { public var requiresRefresh: Bool { + guard let expirationDate else { + return false + } return Date() > expirationDate } }