Skip to content

Commit

Permalink
Merge pull request #92 from Infomaniak/refactor-login
Browse files Browse the repository at this point in the history
refactor: Move login code back to Login package
  • Loading branch information
PhilippeWeidmann authored Jan 16, 2024
2 parents 504932a + dc0c526 commit a1341fb
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 324 deletions.
15 changes: 12 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,6 +27,15 @@
"version" : "2.0.1"
}
},
{
"identity" : "ios-login",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/ios-login",
"state" : {
"revision" : "904c1ac39b4db56212302b464a0b2e023d9b5791",
"version" : "6.0.0"
}
},
{
"identity" : "realm-core",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -59,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"
}
},
{
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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", .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")),
Expand All @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions Sources/InfomaniakCore/Account/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import Foundation
import InfomaniakLogin

public protocol AccountUpdateDelegate {
func didUpdateCurrentAccount(_ account: Account)
Expand Down
90 changes: 58 additions & 32 deletions Sources/InfomaniakCore/Account/KeychainHelper.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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,
Expand All @@ -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 {
Expand All @@ -66,7 +67,7 @@ public class KeychainHelper {
return false
}
}

func initKeychainAccessibility() {
accessibilityValueWritten = true
let queryAdd: [String: Any] = [
Expand All @@ -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] = [
Expand All @@ -93,7 +94,7 @@ public class KeychainHelper {
DDLogInfo("Successfully deleted token ? \(resultCode == noErr)")
}
}

public func deleteAllTokens() {
keychainQueue.sync {
let queryDelete: [String: Any] = [
Expand All @@ -104,27 +105,38 @@ 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 {
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 {
Expand All @@ -149,7 +161,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 {
Expand All @@ -164,11 +176,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],
Expand All @@ -179,7 +191,7 @@ public class KeychainHelper {
}
return savedToken
}

public func loadTokens() -> [ApiToken] {
var values = [ApiToken]()
keychainQueue.sync {
Expand All @@ -192,14 +204,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"
Expand All @@ -208,7 +220,7 @@ public class KeychainHelper {
SentrySDK.addBreadcrumb(crumb)
return
}

if let array = result as? [[String: Any]] {
let jsonDecoder = JSONDecoder()
for item in array {
Expand All @@ -226,3 +238,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 ?? "infinite",
"Access Token": truncatedAccessToken,
"Refresh Token": truncatedRefreshToken,
"Keychain error code": keychainError]
return crumb
}
}
12 changes: 8 additions & 4 deletions Sources/InfomaniakCore/Networking/ApiFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import Alamofire
import Foundation
import InfomaniakDI
import InfomaniakLogin
import Sentry

public protocol RefreshTokenDelegate: AnyObject {
Expand All @@ -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<Int> = {
var allStatus = Set(200 ... 500)
allStatus.remove(401)
return allStatus
}()

public var authenticatedSession: Session!
public static var decoder: JSONDecoder = {
let decoder = JSONDecoder()
Expand Down Expand Up @@ -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<T>.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)
}
Expand Down Expand Up @@ -214,6 +215,9 @@ open class OAuthAuthenticator: Authenticator {

extension ApiToken: AuthenticationCredential {
public var requiresRefresh: Bool {
guard let expirationDate else {
return false
}
return Date() > expirationDate
}
}
Expand Down
Loading

0 comments on commit a1341fb

Please sign in to comment.