TF-2384 Handle refresh token when token expired in NSE

Signed-off-by: dab246 <tdvu@linagora.com>
(cherry picked from commit c8a39bb42792770c001ac90d6301d53126225d95)
This commit is contained in:
dab246
2023-12-24 21:45:42 +07:00
committed by Dat H. Pham
parent 5bf1b97de3
commit b1b2bb90a5
19 changed files with 416 additions and 37 deletions
+69 -7
View File
@@ -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 = "<group>"; };
F5D4EA042B2ABF090090DDFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F5D4EA0C2B2ABF970090DDFC /* TwakeMailNSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TwakeMailNSE.entitlements; sourceTree = "<group>"; };
F5E7D8782B38763B0009BB8A /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
F5E7D87F2B3876DE0009BB8A /* Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = "<group>"; };
F5E7D8812B3876F60009BB8A /* AuthenticationCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCredential.swift; sourceTree = "<group>"; };
F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSO.swift; sourceTree = "<group>"; };
F5E7D8852B3877390009BB8A /* TokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenResponse.swift; sourceTree = "<group>"; };
F5E7D8872B38775C0009BB8A /* TokenRefreshManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefreshManager.swift; sourceTree = "<group>"; };
F5E7D8892B3877D30009BB8A /* NSEPusher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEPusher.swift; sourceTree = "<group>"; };
F5E7D88C2B3878580009BB8A /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationInterceptor.swift; sourceTree = "<group>"; };
F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwakeLogger.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@@ -324,6 +349,7 @@
F5630C7D2B2CE359003CC0FD /* InfoPlistReader.swift */,
F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */,
F5630C862B2D0387003CC0FD /* PayloadParser.swift */,
F5E7D8892B3877D30009BB8A /* NSEPusher.swift */,
);
path = Utils;
sourceTree = "<group>";
@@ -337,6 +363,19 @@
path = Exceptions;
sourceTree = "<group>";
};
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 = "<group>";
};
F5D4EA012B2ABF090090DDFC /* TwakeMailNSE */ = {
isa = PBXGroup;
children = (
@@ -350,13 +389,21 @@
path = TwakeMailNSE;
sourceTree = "<group>";
};
F5E7D8762B385B430009BB8A /* Model */ = {
F5E7D8772B38761B0009BB8A /* Extensions */ = {
isa = PBXGroup;
children = (
F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */,
F53D1E892B2E4BB700051FD0 /* TokenOidc.swift */,
F5E7D8782B38763B0009BB8A /* DateExtensions.swift */,
F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */,
);
path = Model;
path = Extensions;
sourceTree = "<group>";
};
F5E7D88B2B3878470009BB8A /* Interceptor */ = {
isa = PBXGroup;
children = (
F5E7D88C2B3878580009BB8A /* AuthenticationInterceptor.swift */,
);
path = Interceptor;
sourceTree = "<group>";
};
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;
};
+7 -2
View File
@@ -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) {
@@ -0,0 +1,7 @@
import Foundation
extension Date {
func isBefore(_ otherDate: Date) -> Bool {
return self < otherDate
}
}
@@ -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
}
}
+37 -17
View File
@@ -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<Email>) in
if let listEmail = data.parsing(methodName: JmapConstants.EMAIL_GET_METHOD_NAME, methodCallId: "c1"), !listEmail.isEmpty {
onSuccess(listEmail)
@@ -15,4 +15,6 @@ class JmapConstants {
"preview",
"from"
]
static let EMAIL_ID = "email_id"
}
+2 -1
View File
@@ -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)
@@ -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<URLRequest, Error>) -> 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)
}
}
@@ -0,0 +1,7 @@
import Foundation
protocol Authentication {
var type: AuthenticationType { get }
func getAuthenticationHeader() -> String
}
@@ -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)"
}
}
@@ -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
}
}
+1 -7
View File
@@ -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)"
}
}
@@ -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
}
}
@@ -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)"))
}
}
}
}
}
@@ -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 {}
}
}
@@ -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
)
}
+3 -1
View File
@@ -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()
}
@@ -20,6 +20,8 @@ class KeychainSharingSession with EquatableMixin {
String? emailState;
TokenOIDC? tokenOIDC;
String? basicAuth;
String? tokenEndpoint;
List<String>? 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<String, dynamic> json) => _$KeychainSharingSessionFromJson(json);
@@ -44,5 +48,7 @@ class KeychainSharingSession with EquatableMixin {
emailState,
tokenOIDC,
basicAuth,
tokenEndpoint,
oidcScopes,
];
}
}
@@ -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);