From b1b2bb90a569bf711bf1dfb7cb767019504168bf Mon Sep 17 00:00:00 2001 From: dab246 Date: Sun, 24 Dec 2023 21:45:42 +0700 Subject: [PATCH] TF-2384 Handle refresh token when token expired in NSE Signed-off-by: dab246 (cherry picked from commit c8a39bb42792770c001ac90d6301d53126225d95) --- ios/Runner.xcodeproj/project.pbxproj | 76 +++++++++++++++-- ios/Runner/AppDelegate.swift | 9 ++- ios/TwakeCore/Extensions/DateExtensions.swift | 7 ++ .../Extensions/StringExtensions.swift | 12 +++ ios/TwakeCore/Jmap/JmapClient.swift | 54 +++++++++---- ios/TwakeCore/Jmap/Utils/JmapConstants.swift | 2 + ios/TwakeCore/Network/AlamofireService.swift | 3 +- .../AuthenticationInterceptor.swift | 81 +++++++++++++++++++ .../Network/Model/Authentication.swift | 7 ++ .../Model/AuthenticationCredential.swift | 16 ++++ .../Network/Model/AuthenticationSSO.swift | 33 ++++++++ ios/TwakeCore/Network/Model/TokenOidc.swift | 8 +- .../Network/Model/TokenResponse.swift | 25 ++++++ .../Network/TokenRefreshManager.swift | 69 ++++++++++++++++ .../Keychain/KeychainController.swift | 9 +++ .../Model/KeychainSharingSession.swift | 25 +++++- ios/TwakeMailNSE/NotificationService.swift | 4 +- .../keychain/keychain_sharing_session.dart | 8 +- .../response/oidc_discovery_response.dart | 5 ++ 19 files changed, 416 insertions(+), 37 deletions(-) create mode 100644 ios/TwakeCore/Extensions/DateExtensions.swift create mode 100644 ios/TwakeCore/Extensions/StringExtensions.swift create mode 100644 ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift create mode 100644 ios/TwakeCore/Network/Model/Authentication.swift create mode 100644 ios/TwakeCore/Network/Model/AuthenticationCredential.swift create mode 100644 ios/TwakeCore/Network/Model/AuthenticationSSO.swift create mode 100644 ios/TwakeCore/Network/Model/TokenResponse.swift create mode 100644 ios/TwakeCore/Network/TokenRefreshManager.swift diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7eb6d9230..b8bd6d684 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -38,11 +38,24 @@ F5BBBF512B2EEC37007930E1 /* NetworkExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF502B2EEC37007930E1 /* NetworkExceptions.swift */; }; F5BBBF532B2EECAA007930E1 /* JmapExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF522B2EECAA007930E1 /* JmapExceptions.swift */; }; F5BBBF552B2EEF3D007930E1 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */; }; + F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; }; + F5CFE0352B335322005A90A9 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D4EA022B2ABF090090DDFC /* NotificationService.swift */; }; F5D4EA072B2ABF090090DDFC /* TwakeMailNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F5D4EA002B2ABF090090DDFC /* TwakeMailNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F5E7D8742B3578F90009BB8A /* JmapConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D1E7E2B2E3C2600051FD0 /* JmapConstants.swift */; }; + F5E7D8792B38763B0009BB8A /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8782B38763B0009BB8A /* DateExtensions.swift */; }; + F5E7D87A2B38763B0009BB8A /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8782B38763B0009BB8A /* DateExtensions.swift */; }; + F5E7D87C2B38764F0009BB8A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */; }; + F5E7D87D2B38764F0009BB8A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */; }; + F5E7D8802B3876DE0009BB8A /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D87F2B3876DE0009BB8A /* Authentication.swift */; }; + F5E7D8822B3876F60009BB8A /* AuthenticationCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8812B3876F60009BB8A /* AuthenticationCredential.swift */; }; + F5E7D8842B3877050009BB8A /* AuthenticationSSO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */; }; + F5E7D8862B3877390009BB8A /* TokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8852B3877390009BB8A /* TokenResponse.swift */; }; + F5E7D8882B38775C0009BB8A /* TokenRefreshManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8872B38775C0009BB8A /* TokenRefreshManager.swift */; }; + F5E7D88A2B3877D30009BB8A /* NSEPusher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8892B3877D30009BB8A /* NSEPusher.swift */; }; + F5E7D88D2B3878580009BB8A /* AuthenticationInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D88C2B3878580009BB8A /* AuthenticationInterceptor.swift */; }; F5EFC07D2B328B9F00829056 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; }; F5EFC07E2B328B9F00829056 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; }; - F5EFC07F2B3292FA00829056 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -135,6 +148,15 @@ F5D4EA022B2ABF090090DDFC /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; F5D4EA042B2ABF090090DDFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5D4EA0C2B2ABF970090DDFC /* TwakeMailNSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TwakeMailNSE.entitlements; sourceTree = ""; }; + F5E7D8782B38763B0009BB8A /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; + F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; + F5E7D87F2B3876DE0009BB8A /* Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = ""; }; + F5E7D8812B3876F60009BB8A /* AuthenticationCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCredential.swift; sourceTree = ""; }; + F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSO.swift; sourceTree = ""; }; + F5E7D8852B3877390009BB8A /* TokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenResponse.swift; sourceTree = ""; }; + F5E7D8872B38775C0009BB8A /* TokenRefreshManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefreshManager.swift; sourceTree = ""; }; + F5E7D8892B3877D30009BB8A /* NSEPusher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEPusher.swift; sourceTree = ""; }; + F5E7D88C2B3878580009BB8A /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationInterceptor.swift; sourceTree = ""; }; F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwakeLogger.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -144,6 +166,7 @@ buildActionMask = 2147483647; files = ( CDFECA8C54311B749F044831 /* Pods_Runner.framework in Frameworks */, + F5CFE0352B335322005A90A9 /* (null) in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -240,8 +263,10 @@ F53D1E642B2DE84E00051FD0 /* Network */ = { isa = PBXGroup; children = ( - F5E7D8762B385B430009BB8A /* Model */, + F5E7D88B2B3878470009BB8A /* Interceptor */, + F5CFE0182B334BCD005A90A9 /* Model */, F53D1E652B2DE8B800051FD0 /* AlamofireService.swift */, + F5E7D8872B38775C0009BB8A /* TokenRefreshManager.swift */, ); path = Network; sourceTree = ""; @@ -324,6 +349,7 @@ F5630C7D2B2CE359003CC0FD /* InfoPlistReader.swift */, F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */, F5630C862B2D0387003CC0FD /* PayloadParser.swift */, + F5E7D8892B3877D30009BB8A /* NSEPusher.swift */, ); path = Utils; sourceTree = ""; @@ -337,6 +363,19 @@ path = Exceptions; sourceTree = ""; }; + F5CFE0182B334BCD005A90A9 /* Model */ = { + isa = PBXGroup; + children = ( + F53D1E892B2E4BB700051FD0 /* TokenOidc.swift */, + F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */, + F5E7D87F2B3876DE0009BB8A /* Authentication.swift */, + F5E7D8812B3876F60009BB8A /* AuthenticationCredential.swift */, + F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */, + F5E7D8852B3877390009BB8A /* TokenResponse.swift */, + ); + path = Model; + sourceTree = ""; + }; F5D4EA012B2ABF090090DDFC /* TwakeMailNSE */ = { isa = PBXGroup; children = ( @@ -350,13 +389,21 @@ path = TwakeMailNSE; sourceTree = ""; }; - F5E7D8762B385B430009BB8A /* Model */ = { + F5E7D8772B38761B0009BB8A /* Extensions */ = { isa = PBXGroup; children = ( - F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */, - F53D1E892B2E4BB700051FD0 /* TokenOidc.swift */, + F5E7D8782B38763B0009BB8A /* DateExtensions.swift */, + F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */, ); - path = Model; + path = Extensions; + sourceTree = ""; + }; + F5E7D88B2B3878470009BB8A /* Interceptor */ = { + isa = PBXGroup; + children = ( + F5E7D88C2B3878580009BB8A /* AuthenticationInterceptor.swift */, + ); + path = Interceptor; sourceTree = ""; }; F5EFC07B2B328B8600829056 /* Logger */ = { @@ -370,6 +417,7 @@ F5EFC0802B32965100829056 /* TwakeCore */ = { isa = PBXGroup; children = ( + F5E7D8772B38761B0009BB8A /* Extensions */, F5BBBF4F2B2EEC1B007930E1 /* Exceptions */, F53D1E642B2DE84E00051FD0 /* Network */, F5EFC07B2B328B8600829056 /* Logger */, @@ -412,6 +460,8 @@ F5D4EA062B2ABF090090DDFC /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -607,9 +657,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F5E7D87C2B38764F0009BB8A /* StringExtensions.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + F5E7D8792B38763B0009BB8A /* DateExtensions.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, F5EFC07D2B328B9F00829056 /* TwakeLogger.swift in Sources */, + F5E7D8742B3578F90009BB8A /* JmapConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -617,7 +670,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F5EFC07F2B3292FA00829056 /* TwakeLogger.swift in Sources */, + F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */, F52F993027FD6EB900346091 /* ShareViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -628,15 +681,22 @@ files = ( F5BBBF512B2EEC37007930E1 /* NetworkExceptions.swift in Sources */, F53D1E7F2B2E3C2600051FD0 /* JmapConstants.swift in Sources */, + F5E7D8822B3876F60009BB8A /* AuthenticationCredential.swift in Sources */, F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */, F53D1E8A2B2E4BB700051FD0 /* TokenOidc.swift in Sources */, F53D1E862B2E401B00051FD0 /* JmapRequestGenerator.swift in Sources */, + F5E7D87A2B38763B0009BB8A /* DateExtensions.swift in Sources */, + F5E7D88D2B3878580009BB8A /* AuthenticationInterceptor.swift in Sources */, + F5E7D88A2B3877D30009BB8A /* NSEPusher.swift in Sources */, F5630C7E2B2CE359003CC0FD /* InfoPlistReader.swift in Sources */, F53D1E662B2DE8B800051FD0 /* AlamofireService.swift in Sources */, F5BBBF532B2EECAA007930E1 /* JmapExceptions.swift in Sources */, F53D1E882B2E4B3B00051FD0 /* AuthenticationType.swift in Sources */, F53D1E6C2B2E208C00051FD0 /* Email.swift in Sources */, F5630C872B2D0387003CC0FD /* PayloadParser.swift in Sources */, + F5E7D8862B3877390009BB8A /* TokenResponse.swift in Sources */, + F5E7D8842B3877050009BB8A /* AuthenticationSSO.swift in Sources */, + F5E7D87D2B38764F0009BB8A /* StringExtensions.swift in Sources */, F53D1E782B2E2C7E00051FD0 /* JmapResponseObject.swift in Sources */, F5630C852B2CF50C003CC0FD /* TypeName.swift in Sources */, F5630C7B2B2CE148003CC0FD /* KeychainSharingSession.swift in Sources */, @@ -644,9 +704,11 @@ F5BBBF552B2EEF3D007930E1 /* BundleExtension.swift in Sources */, F53D1E7C2B2E35CC00051FD0 /* EmailAddress.swift in Sources */, F5EFC07E2B328B9F00829056 /* TwakeLogger.swift in Sources */, + F5E7D8882B38775C0009BB8A /* TokenRefreshManager.swift in Sources */, F5630C762B2CDFDA003CC0FD /* KeychainController.swift in Sources */, F53D1E832B2E3D4600051FD0 /* JmapRequestObject.swift in Sources */, F5630C782B2CE091003CC0FD /* KeyChainControllerDelegate.swift in Sources */, + F5E7D8802B3876DE0009BB8A /* Authentication.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 961882587..4c34d1dd0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -84,8 +84,13 @@ import flutter_local_notifications TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent:newBadgeCount: \(newBadgeCount)") updateAppBadger(currentBadgeCount: newBadgeCount) } - - completionHandler(isAppForegroundActive() ? [] : [.alert, .badge, .sound]) + if let emailId = notification.request.content.userInfo[JmapConstants.EMAIL_ID] as? String, + !emailId.isEmpty, + !isAppForegroundActive() { + completionHandler([.alert, .badge, .sound]) + } else { + completionHandler([]) + } } override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { diff --git a/ios/TwakeCore/Extensions/DateExtensions.swift b/ios/TwakeCore/Extensions/DateExtensions.swift new file mode 100644 index 000000000..b610c1bcd --- /dev/null +++ b/ios/TwakeCore/Extensions/DateExtensions.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Date { + func isBefore(_ otherDate: Date) -> Bool { + return self < otherDate + } +} diff --git a/ios/TwakeCore/Extensions/StringExtensions.swift b/ios/TwakeCore/Extensions/StringExtensions.swift new file mode 100644 index 000000000..5c6c2d011 --- /dev/null +++ b/ios/TwakeCore/Extensions/StringExtensions.swift @@ -0,0 +1,12 @@ +import Foundation + +extension String { + func convertISO8601StringToDate() -> Date? { + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = dateFormatter.date(from: self) { + return date + } + return nil + } +} diff --git a/ios/TwakeCore/Jmap/JmapClient.swift b/ios/TwakeCore/Jmap/JmapClient.swift index ac9c83e34..cfac87c69 100644 --- a/ios/TwakeCore/Jmap/JmapClient.swift +++ b/ios/TwakeCore/Jmap/JmapClient.swift @@ -3,13 +3,13 @@ import Alamofire class JmapClient { static let shared: JmapClient = JmapClient() - + private let jmapHeader = HTTPHeader(name: "Accept", value: "application/json;jmapVersion=rfc-8621") - - private func getBasicAuthorization(basicAuth: String) -> String { - return "Basic \(basicAuth)" - } - + + private var authentication: Authentication? + private var tokenRefreshManager: TokenRefreshManager? + private var authenticationInterceptor: AuthenticationInterceptor? + func getNewEmails( apiUrl: String, accountId: String, @@ -17,27 +17,47 @@ class JmapClient { authenticationType: AuthenticationType, tokenOidc: TokenOidc?, basicAuth: String?, + tokenEndpointUrl: String?, + oidcScopes: [String]?, onSuccess: @escaping ([Email]) -> Void, onFailure: @escaping (Error) -> Void ) { - let authenticationValue = authenticationType == AuthenticationType.basic - ? getBasicAuthorization(basicAuth: basicAuth ?? "") - : tokenOidc?.getAuthorization() ?? "" - - let headers = HTTPHeaders([ - jmapHeader, - HTTPHeader(name: "Authorization", value: authenticationValue) - ]) - + if (authenticationType == AuthenticationType.basic) { + authentication = AuthenticationCredential( + type: AuthenticationType.basic, + basicAuth: basicAuth ?? "" + ) + } else { + authentication = AuthenticationSSO( + type: AuthenticationType.oidc, + accessToken: tokenOidc?.token ?? "", + refreshToken: tokenOidc?.refreshToken ?? "", + expireTime: tokenOidc?.expiredTime + ) + + tokenRefreshManager = TokenRefreshManager( + refreshToken: tokenOidc?.refreshToken ?? "", + tokenEndpoint: tokenEndpointUrl ?? "", + scopes: oidcScopes + ) + } + + authenticationInterceptor = AuthenticationInterceptor( + authentication: authentication, + accountId: accountId, + tokenRefreshManager: tokenRefreshManager + ) + let jmapRequestObject = JmapRequestGenerator.shared.createEmailChangesRequest( accountId: accountId, sinceState: sinceState ) - + AlamofireService.shared.post( url: apiUrl, payloadData: jmapRequestObject?.toData(), - headers: headers, + headers: HTTPHeaders([jmapHeader]), + interceptor: authenticationInterceptor, onSuccess: { (data: JmapResponseObject) in if let listEmail = data.parsing(methodName: JmapConstants.EMAIL_GET_METHOD_NAME, methodCallId: "c1"), !listEmail.isEmpty { onSuccess(listEmail) diff --git a/ios/TwakeCore/Jmap/Utils/JmapConstants.swift b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift index 11b6e4600..d5b6b31d1 100644 --- a/ios/TwakeCore/Jmap/Utils/JmapConstants.swift +++ b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift @@ -15,4 +15,6 @@ class JmapConstants { "preview", "from" ] + + static let EMAIL_ID = "email_id" } diff --git a/ios/TwakeCore/Network/AlamofireService.swift b/ios/TwakeCore/Network/AlamofireService.swift index 892d0f06d..64e6d1c16 100644 --- a/ios/TwakeCore/Network/AlamofireService.swift +++ b/ios/TwakeCore/Network/AlamofireService.swift @@ -8,6 +8,7 @@ class AlamofireService { url: String, payloadData: Data?, headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, onSuccess: @escaping (T) -> Void, onFailure: @escaping (Error) -> Void ) { @@ -18,7 +19,7 @@ class AlamofireService { request.httpBody = payloadData - AF.request(request).responseDecodable(of: T.self) { response in + AF.request(request, interceptor: interceptor).validate().responseDecodable(of: T.self) { response in switch(response.result) { case .success(let data): onSuccess(data) diff --git a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift new file mode 100644 index 000000000..5e17bfef4 --- /dev/null +++ b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift @@ -0,0 +1,81 @@ +import Alamofire +import Foundation + +class AuthenticationInterceptor: RequestInterceptor { + var authentication: Authentication? + let accountId: String + var tokenRefreshManager: TokenRefreshManager? + + private lazy var keychainController = KeychainController(service: .sessions, + accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) + + init(authentication: Authentication?, accountId: String, tokenRefreshManager: TokenRefreshManager?) { + self.authentication = authentication + self.accountId = accountId + self.tokenRefreshManager = tokenRefreshManager + } + + // MARK: - Adapt: Called before request to set request headers + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + var modifiedRequest = urlRequest + + if (authentication != nil) { + modifiedRequest.addValue(authentication!.getAuthenticationHeader(), forHTTPHeaderField: "Authorization") + } + + completion(.success(modifiedRequest)) + } + + // MARK: - Retry: Called when status code is not 200...299 or in failure + func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { + guard let response = request.task?.response as? HTTPURLResponse, + let authenticationSSO = authentication as? AuthenticationSSO, + validateToRefreshToken(response: response, authenticationSSO: authenticationSSO), + let tokenRefreshManager else { + return completion(.doNotRetryWithError(error)) + } + + handleRefreshToken(tokenRefreshManager: tokenRefreshManager) { tokenResponse in + guard let accessToken = tokenResponse.accessToken, + let refreshToken = tokenResponse.refreshToken else { + return completion(.doNotRetryWithError(error)) + } + + self.authentication = AuthenticationSSO( + type: AuthenticationType.oidc, + accessToken: accessToken, + refreshToken: refreshToken, + expireTime: "\(tokenResponse.expiresTime ?? 0)" + ) + + self.keychainController.updateTokenOidc( + accountId: self.accountId, + newTokenOidc: TokenOidc( + token: accessToken, + tokenId: tokenResponse.tokenId, + expiredTime: "\(tokenResponse.expiresTime ?? 0)", + refreshToken: refreshToken + ) + ) + + return completion(.retry) + } onFailure: { error in + return completion(.doNotRetryWithError(error)) + } + + } + + // MARK: - Check the conditions to perform token refresh + private func validateToRefreshToken(response: HTTPURLResponse, authenticationSSO: AuthenticationSSO) -> Bool { + return response.statusCode == 401 && + !authenticationSSO.refreshToken.isEmpty && + authenticationSSO.isExpiredTime() + } + + // MARK: - Handle refresh token to get new token + private func handleRefreshToken(tokenRefreshManager: TokenRefreshManager, + onSuccess: @escaping (TokenResponse) -> Void, + onFailure: @escaping (Error) -> Void) { + tokenRefreshManager.handleRefreshAccessToken(onSuccess: onSuccess, onFailure: onFailure) + } +} diff --git a/ios/TwakeCore/Network/Model/Authentication.swift b/ios/TwakeCore/Network/Model/Authentication.swift new file mode 100644 index 000000000..19ff3014f --- /dev/null +++ b/ios/TwakeCore/Network/Model/Authentication.swift @@ -0,0 +1,7 @@ +import Foundation + +protocol Authentication { + var type: AuthenticationType { get } + + func getAuthenticationHeader() -> String +} diff --git a/ios/TwakeCore/Network/Model/AuthenticationCredential.swift b/ios/TwakeCore/Network/Model/AuthenticationCredential.swift new file mode 100644 index 000000000..39147f05c --- /dev/null +++ b/ios/TwakeCore/Network/Model/AuthenticationCredential.swift @@ -0,0 +1,16 @@ +import Foundation + +struct AuthenticationCredential: Authentication { + var type: AuthenticationType + + let basicAuth: String + + init(type: AuthenticationType, basicAuth: String) { + self.type = type + self.basicAuth = basicAuth + } + + func getAuthenticationHeader() -> String { + return "Basic \(basicAuth)" + } +} diff --git a/ios/TwakeCore/Network/Model/AuthenticationSSO.swift b/ios/TwakeCore/Network/Model/AuthenticationSSO.swift new file mode 100644 index 000000000..29df16863 --- /dev/null +++ b/ios/TwakeCore/Network/Model/AuthenticationSSO.swift @@ -0,0 +1,33 @@ +import Foundation + +struct AuthenticationSSO: Authentication { + var type: AuthenticationType + + let accessToken: String + let refreshToken: String + let expireTime: String? + + init(type: AuthenticationType, accessToken: String, refreshToken: String, expireTime: String?) { + self.type = type + self.accessToken = accessToken + self.refreshToken = refreshToken + self.expireTime = expireTime + } + + func getAuthenticationHeader() -> String { + return "Bearer \(accessToken)" + } + + func isExpiredTime() -> Bool { + guard let expireTime else { + return false + } + + guard let expireDate = expireTime.convertISO8601StringToDate(), + expireDate.isBefore(Date.now) else { + return true + } + + return false + } +} diff --git a/ios/TwakeCore/Network/Model/TokenOidc.swift b/ios/TwakeCore/Network/Model/TokenOidc.swift index 8aebc23c2..bbfd2a160 100644 --- a/ios/TwakeCore/Network/Model/TokenOidc.swift +++ b/ios/TwakeCore/Network/Model/TokenOidc.swift @@ -2,13 +2,7 @@ import Foundation struct TokenOidc: Codable { let token: String - let tokenId: String + let tokenId: String? let expiredTime: String? let refreshToken: String } - -extension TokenOidc { - func getAuthorization() -> String { - return "Bearer \(token)" - } -} diff --git a/ios/TwakeCore/Network/Model/TokenResponse.swift b/ios/TwakeCore/Network/Model/TokenResponse.swift new file mode 100644 index 000000000..a909d60a6 --- /dev/null +++ b/ios/TwakeCore/Network/Model/TokenResponse.swift @@ -0,0 +1,25 @@ +import Foundation + +struct TokenResponse: Codable { + let accessToken: String? + let refreshToken: String? + let expiresTime: Int? + let refreshExpiresIn: Int? + let tokenId: String? + let tokenType: String? + let scope: String? + let sessionState: String? + let error: String? + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case refreshToken = "refresh_token" + case expiresTime = "expires_in" + case refreshExpiresIn = "refresh_expires_in" + case tokenId = "id_token" + case tokenType = "token_type" + case scope + case sessionState = "session_state" + case error + } +} diff --git a/ios/TwakeCore/Network/TokenRefreshManager.swift b/ios/TwakeCore/Network/TokenRefreshManager.swift new file mode 100644 index 000000000..83dbfbb0c --- /dev/null +++ b/ios/TwakeCore/Network/TokenRefreshManager.swift @@ -0,0 +1,69 @@ +import Foundation +import Alamofire + +class TokenRefreshManager { + private let MOBIE_CLIENT_ID = "teammail-mobile" + private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect" + private let OIDC_SCOPES = ["openid", "profile", "email", "offline_access"] + + private let GRANT_TYPE = "grant_type" + private let REFRESH_TOKEN = "refresh_token" + private let CLIENT_ID = "client_id" + private let REDIRECT_URI = "redirect_uri" + private let SCOPES = "scopes" + + let refreshToken: String + let tokenEndpoint: String + let scopes: [String]? + + init(refreshToken: String, tokenEndpoint: String, scopes: [String]?) { + self.refreshToken = refreshToken + self.tokenEndpoint = tokenEndpoint + self.scopes = scopes + } + + private func getScopes() -> String { + if let scopes, !scopes.isEmpty { + return scopes.joined(separator: " ") + } + return OIDC_SCOPES.joined(separator: " ") + } + + func handleRefreshAccessToken( + onSuccess: @escaping (TokenResponse) -> Void, + onFailure: @escaping (Error) -> Void + ) { + guard let tokenEndpointUrl = URL(string: tokenEndpoint), + var request = try? URLRequest(url: tokenEndpointUrl, method: .post) else { + return onFailure(NetworkExceptions.requestUrlInvalid) + } + + let params = [ + CLIENT_ID: MOBIE_CLIENT_ID, + GRANT_TYPE: REFRESH_TOKEN, + REDIRECT_URI: MOBIE_REDIRECT_URL, + REFRESH_TOKEN: refreshToken, + SCOPES: getScopes(), + ] + + let data = params + .map { "\($0)=\($1)" } + .joined(separator: "&") + .data(using: .utf8) + + request.httpBody = data + + AF.request(request).responseDecodable(of: TokenResponse.self) { response in + if (response.response?.statusCode != 200) { + onFailure(NetworkExceptions(value: "Failed to get token: \(response.error?.localizedDescription ?? "Unknown Error")")) + } else { + switch(response.result) { + case .success(let data): + onSuccess(data) + case .failure(let error): + onFailure(NetworkExceptions(value: "Failed to get token \(error.localizedDescription)")) + } + } + } + } +} diff --git a/ios/TwakeMailNSE/Keychain/KeychainController.swift b/ios/TwakeMailNSE/Keychain/KeychainController.swift index eb15d2611..a5bd00c6e 100644 --- a/ios/TwakeMailNSE/Keychain/KeychainController.swift +++ b/ios/TwakeMailNSE/Keychain/KeychainController.swift @@ -48,4 +48,13 @@ class KeychainController: KeychainControllerDelegate { } } catch {} } + + func updateTokenOidc(accountId: String, newTokenOidc: TokenOidc) { + do { + if let sharingSession = retrieveSharingSessionFromKeychain(accountId: accountId) { + let newSharingSession = sharingSession.updateTokenOidc(newTokenOidc: newTokenOidc) + try keychain.set(newSharingSession.toJson() ?? "", key: accountId) + } + } catch {} + } } diff --git a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift index 35f8e43c5..0a00c88d4 100644 --- a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift +++ b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift @@ -8,6 +8,8 @@ struct KeychainSharingSession: Codable { let emailState: String? let tokenOIDC: TokenOidc? let basicAuth: String? + let tokenEndpoint: String? + let oidcScopes: [String]? } extension KeychainSharingSession { @@ -19,7 +21,28 @@ extension KeychainSharingSession { apiUrl: self.apiUrl, emailState: newState, tokenOIDC: self.tokenOIDC, - basicAuth: self.basicAuth + basicAuth: self.basicAuth, + tokenEndpoint: self.tokenEndpoint, + oidcScopes: self.oidcScopes + ) + } + + func updateTokenOidc(newTokenOidc: TokenOidc) -> KeychainSharingSession { + return KeychainSharingSession( + accountId: self.accountId, + userName: self.userName, + authenticationType: self.authenticationType, + apiUrl: self.apiUrl, + emailState: self.emailState, + tokenOIDC: TokenOidc( + token: newTokenOidc.token, + tokenId: newTokenOidc.tokenId, + expiredTime: newTokenOidc.expiredTime, + refreshToken: newTokenOidc.refreshToken + ), + basicAuth: self.basicAuth, + tokenEndpoint: self.tokenEndpoint, + oidcScopes: self.oidcScopes ) } diff --git a/ios/TwakeMailNSE/NotificationService.swift b/ios/TwakeMailNSE/NotificationService.swift index 55d9da03c..1b0746fce 100644 --- a/ios/TwakeMailNSE/NotificationService.swift +++ b/ios/TwakeMailNSE/NotificationService.swift @@ -61,6 +61,8 @@ class NotificationService: UNNotificationServiceExtension { authenticationType: keychainSharingSession.authenticationType, tokenOidc: keychainSharingSession.tokenOIDC, basicAuth: keychainSharingSession.basicAuth, + tokenEndpointUrl: keychainSharingSession.tokenEndpoint, + oidcScopes: keychainSharingSession.oidcScopes, onSuccess: { emails in self.keychainController.updateEmailStateToKeychain(accountId: keychainSharingSession.accountId, newState: newEmailState) @@ -90,7 +92,7 @@ class NotificationService: UNNotificationServiceExtension { } }, onFailure: { error in - self.modifiedContent?.body = newEmailDefaultMessage + self.modifiedContent?.body = self.newEmailDefaultMessage self.modifiedContent?.badge = NSNumber(value: 1) return self.notify() } diff --git a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart index 90c7d7deb..e0e6666dc 100644 --- a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart +++ b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart @@ -20,6 +20,8 @@ class KeychainSharingSession with EquatableMixin { String? emailState; TokenOIDC? tokenOIDC; String? basicAuth; + String? tokenEndpoint; + List? oidcScopes; KeychainSharingSession({ required this.accountId, @@ -29,6 +31,8 @@ class KeychainSharingSession with EquatableMixin { this.emailState, this.tokenOIDC, this.basicAuth, + this.tokenEndpoint, + this.oidcScopes, }); factory KeychainSharingSession.fromJson(Map json) => _$KeychainSharingSessionFromJson(json); @@ -44,5 +48,7 @@ class KeychainSharingSession with EquatableMixin { emailState, tokenOIDC, basicAuth, + tokenEndpoint, + oidcScopes, ]; -} +} \ No newline at end of file diff --git a/model/lib/oidc/response/oidc_discovery_response.dart b/model/lib/oidc/response/oidc_discovery_response.dart index 9553f3a05..76700cd67 100644 --- a/model/lib/oidc/response/oidc_discovery_response.dart +++ b/model/lib/oidc/response/oidc_discovery_response.dart @@ -6,8 +6,13 @@ part 'oidc_discovery_response.g.dart'; @JsonSerializable(explicitToJson: true, includeIfNull: false) class OIDCDiscoveryResponse with EquatableMixin { + @JsonKey(name: 'authorization_endpoint') final String? authorizationEndpoint; + + @JsonKey(name: 'token_endpoint') final String? tokenEndpoint; + + @JsonKey(name: 'end_session_endpoint') final String? endSessionEndpoint; OIDCDiscoveryResponse(this.authorizationEndpoint, this.tokenEndpoint, this.endSessionEndpoint);