From 1478088e193aeac65066e434247990721021a408 Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Thu, 18 Mar 2021 14:16:35 +1100 Subject: [PATCH] COVIDSafe code from version 2.4 (#47) --- CovidSafe.xcodeproj/project.pbxproj | 38 +- CovidSafe/API/AuthenticationAPI.swift | 211 ++++++++ CovidSafe/API/ChangePostcodeAPI.swift | 7 +- CovidSafe/API/CovidNetworking.swift | 22 +- CovidSafe/API/CovidRequestRetrier.swift | 72 ++- CovidSafe/API/GetTempIdAPI.swift | 17 +- CovidSafe/API/InitiateUploadAPI.swift | 6 +- CovidSafe/API/MessageAPI.swift | 18 +- CovidSafe/API/PhoneValidationAPI.swift | 3 +- CovidSafe/API/RespondToAuthChallengeAPI.swift | 18 +- CovidSafe/API/RestrictionsAPI.swift | 8 +- CovidSafe/API/StatisticsAPI.swift | 8 +- CovidSafe/CovidStatisticsViewController.swift | 1 + CovidSafe/Debug.storyboard | 123 ++++- CovidSafe/EncounterMessageManager.swift | 1 - CovidSafe/HomeView.xib | 4 +- CovidSafe/HomeViewController.swift | 3 +- CovidSafe/InfoViewController.swift | 30 ++ CovidSafe/OTPViewController.swift | 6 +- .../RestrictionDetailsViewController.swift | 2 + CovidSafe/WebContentView.xib | 2 +- CovidSafe/ar.lproj/InfoPlist.strings | 6 +- CovidSafe/ar.lproj/Localizable.strings | 64 +-- CovidSafe/el.lproj/InfoPlist.strings | 6 +- CovidSafe/el.lproj/Localizable.strings | 60 ++- CovidSafe/en.lproj/Localizable.strings | 4 + CovidSafe/it.lproj/InfoPlist.strings | 6 +- CovidSafe/it.lproj/Localizable.strings | 64 +-- CovidSafe/ko.lproj/InfoPlist.strings | 14 +- CovidSafe/ko.lproj/Localizable.strings | 170 +++--- CovidSafe/pa-IN.lproj/InfoPlist.strings | 12 +- CovidSafe/pa-IN.lproj/Localizable.strings | 496 +++++++++--------- CovidSafe/tr.lproj/InfoPlist.strings | 6 +- CovidSafe/tr.lproj/Localizable.strings | 64 +-- CovidSafe/vi.lproj/InfoPlist.strings | 6 +- CovidSafe/vi.lproj/Localizable.strings | 64 +-- CovidSafe/zh-Hans.lproj/InfoPlist.strings | 6 +- CovidSafe/zh-Hans.lproj/Localizable.strings | 64 +-- CovidSafe/zh-Hant.lproj/InfoPlist.strings | 6 +- CovidSafe/zh-Hant.lproj/Localizable.strings | 64 +-- 40 files changed, 1125 insertions(+), 657 deletions(-) create mode 100644 CovidSafe/API/AuthenticationAPI.swift diff --git a/CovidSafe.xcodeproj/project.pbxproj b/CovidSafe.xcodeproj/project.pbxproj index d32fa1d..74c3a13 100644 --- a/CovidSafe.xcodeproj/project.pbxproj +++ b/CovidSafe.xcodeproj/project.pbxproj @@ -161,6 +161,8 @@ 5B69C06C25D3983900DF536D /* TableSectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B69C06A25D3983800DF536D /* TableSectionHeaderView.xib */; }; 5B69C07825D4D46800DF536D /* RestrictionsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69C07725D4D46800DF536D /* RestrictionsAPI.swift */; }; 5B69C07925D4D46800DF536D /* RestrictionsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69C07725D4D46800DF536D /* RestrictionsAPI.swift */; }; + 5B69C0D525D9FC4C00DF536D /* AuthenticationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69C0D425D9FC4C00DF536D /* AuthenticationAPI.swift */; }; + 5B69C0D625D9FC4C00DF536D /* AuthenticationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B69C0D425D9FC4C00DF536D /* AuthenticationAPI.swift */; }; 5B728B4724B5667000654ABC /* BLELogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B728B4624B5667000654ABC /* BLELogViewController.swift */; }; 5B728B4924B5816C00654ABC /* BLELog+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B728B4824B5816C00654ABC /* BLELog+CoreDataClass.swift */; }; 5B728B4B24B581C100654ABC /* BLELog+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B728B4A24B581C100654ABC /* BLELog+CoreDataProperties.swift */; }; @@ -510,6 +512,7 @@ 5B69C06525D382AF00DF536D /* String+HtmlAttributed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HtmlAttributed.swift"; sourceTree = ""; }; 5B69C06A25D3983800DF536D /* TableSectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TableSectionHeaderView.xib; sourceTree = ""; }; 5B69C07725D4D46800DF536D /* RestrictionsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestrictionsAPI.swift; sourceTree = ""; }; + 5B69C0D425D9FC4C00DF536D /* AuthenticationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationAPI.swift; sourceTree = ""; }; 5B728B4624B5667000654ABC /* BLELogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BLELogViewController.swift; sourceTree = ""; }; 5B728B4824B5816C00654ABC /* BLELog+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BLELog+CoreDataClass.swift"; sourceTree = ""; }; 5B728B4A24B581C100654ABC /* BLELog+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BLELog+CoreDataProperties.swift"; sourceTree = ""; }; @@ -1040,6 +1043,7 @@ isa = PBXGroup; children = ( 59AF2E97243552FB00ACCAF2 /* Certificates */, + 5B69C0D425D9FC4C00DF536D /* AuthenticationAPI.swift */, FB12C4C0242F047F007E893B /* RespondToAuthChallengeAPI.swift */, 5BFFD94A242EC120003AEF4F /* PhoneValidationAPI.swift */, 5B2E5B1A25AD156E00A021B0 /* ChangePostcodeAPI.swift */, @@ -1618,6 +1622,7 @@ 0B42D0E02432B3AF00E4F44C /* QuestionUploadDataViewController.swift in Sources */, 590546352543E0F6009B82AD /* ContactLog.swift in Sources */, 5B92D6A1243018040049877B /* AlertController.swift in Sources */, + 5B69C0D625D9FC4C00DF536D /* AuthenticationAPI.swift in Sources */, 5B92D750243022F20049877B /* InitiateUploadAPI.swift in Sources */, 5B92D69F243018040049877B /* EncounterMessageManager.swift in Sources */, 590888B82431BD9C008C9B9F /* UploadDataThankYouHomeViewController.swift in Sources */, @@ -1778,6 +1783,7 @@ 5BFFD94B242EC120003AEF4F /* PhoneValidationAPI.swift in Sources */, 59AF2EB22435A38100ACCAF2 /* CovidRequestRetrier.swift in Sources */, 592CBB802441A583001FFCE9 /* PersonalDetailsViewController.swift in Sources */, + 5B69C0D525D9FC4C00DF536D /* AuthenticationAPI.swift in Sources */, 5B89D4DD25D356A400FB3938 /* CovidHeaderContentViewController.swift in Sources */, A767D330242DF1B100DC9E2A /* UIWindow+TopMost.swift in Sources */, D8DEB6822423AE2E00D99925 /* HowItWorksViewController.swift in Sources */, @@ -1967,7 +1973,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -1975,7 +1981,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe; PRODUCT_NAME = COVIDSafe; @@ -2051,7 +2057,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2059,7 +2065,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe; PRODUCT_NAME = COVIDSafe; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2079,7 +2085,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2087,7 +2093,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe.uat; @@ -2109,7 +2115,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2117,7 +2123,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe.uat; @@ -2139,7 +2145,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2147,7 +2153,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe.uat; PRODUCT_MODULE_NAME = COVIDSafe; @@ -2169,7 +2175,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/staging-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2177,7 +2183,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe.uat; PRODUCT_MODULE_NAME = COVIDSafe; @@ -2317,7 +2323,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2325,7 +2331,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe; PRODUCT_NAME = COVIDSafe; @@ -2345,7 +2351,7 @@ CODE_SIGN_ENTITLEMENTS = "CovidSafe/Project Bluetrace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 45792XH5L8; INFOPLIST_FILE = "$(SRCROOT)/CovidSafe/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2353,7 +2359,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; PRODUCT_BUNDLE_IDENTIFIER = au.gov.health.covidsafe; PRODUCT_NAME = COVIDSafe; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CovidSafe/API/AuthenticationAPI.swift b/CovidSafe/API/AuthenticationAPI.swift new file mode 100644 index 0000000..dca9e47 --- /dev/null +++ b/CovidSafe/API/AuthenticationAPI.swift @@ -0,0 +1,211 @@ +// +// AuthenticationAPI.swift +// CovidSafe +// +// Copyright © 2021 Australian Government. All rights reserved. +// + +import Foundation +import Alamofire +import KeychainSwift + +class AuthenticationAPI: CovidSafeAuthenticatedAPI { + + private static func issueRefreshTokenAPI(completion: @escaping (ChallengeResponse?, CovidSafeAPIError?) -> Void) { + guard let apiHost = PlistHelper.getvalueFromInfoPlist(withKey: "API_Host", plistName: "CovidSafe-config") else { + return + } + + CovidNetworking.shared.session.request("\(apiHost)/issueInitialRefreshToken", + method: .post, + encoding: JSONEncoding.default, + headers: authenticatedHeaders + ).validate().responseDecodable(of: ChallengeResponse.self) { (response) in + switch response.result { + case .success: + guard let challengeResponse = response.value else { return } + completion(challengeResponse, nil) + case .failure(_): + + guard let statusCode = response.response?.statusCode else { + completion(nil, .UnknownError) + return + } + + if (statusCode == 200) { + completion(nil, .ResponseError) + return + } + + if statusCode == 401, let respData = response.data { + completion(nil, processUnauthorizedError(respData)) + return + } + if (statusCode >= 400 && statusCode < 500) { + completion(nil, .RequestError) + return + } + completion(nil, .ServerError) + } + } + } + + private static func issueJWTTokenAPI(completion: @escaping (ChallengeResponse?, CovidSafeAPIError?) -> Void) { + guard let apiHost = PlistHelper.getvalueFromInfoPlist(withKey: "API_Host", plistName: "CovidSafe-config") else { + return + } + + let keychain = KeychainSwift() + + guard let token = keychain.get("JWT_TOKEN"), + let refreshToken = keychain.get("REFRESH_TOKEN"), + let subject = AuthenticationToken(token: token).getSubject() else { + completion(nil, .TokenExpiredError) + return + } + + // get params + let params: [String : Any] = [ + "subject" : subject, + "refresh" : refreshToken + ] + + CovidNetworking.shared.session.request("\(apiHost)/reissueAuth", + method: .post, + parameters: params, + encoding: JSONEncoding.default + ).validate().responseDecodable(of: ChallengeResponse.self) { (response) in + switch response.result { + case .success: + guard let challengeResponse = response.value else { return } + completion(challengeResponse, nil) + case .failure(_): + + guard let statusCode = response.response?.statusCode else { + completion(nil, .UnknownError) + return + } + + if (statusCode == 200) { + completion(nil, .ResponseError) + return + } + + if statusCode == 401, let respData = response.data { + completion(nil, processUnauthorizedError(respData)) + return + } + if (statusCode >= 400 && statusCode < 500) { + completion(nil, .RequestError) + return + } + completion(nil, .ServerError) + } + } + } + + static func issueTokensAPI(completion: @escaping (ChallengeResponse?, CovidSafeAPIError?) -> Void) { + let keychain = KeychainSwift() + + // block api call only if refresh token exists, if it doesn't it means the app should get it for the first time + if UserDefaults.standard.bool(forKey: "ReauthenticationNeededKey") && keychain.get("REFRESH_TOKEN") != nil { + completion(nil, .TokenExpiredError) + return + } + + // retrieve and update refresh token + if keychain.get("REFRESH_TOKEN") == nil { + AuthenticationAPI.issueRefreshTokenAPI { (response, error) in + + guard let jwt = response?.token, + let refresh = response?.refreshToken, + error == nil else { + + completion(response, error) + return + } + DLog("Authentication API: JWT and refresh tokens updated. \(jwt)") + + UserDefaults.standard.set(false, forKey: "ReauthenticationNeededKey") + keychain.set(jwt, forKey: "JWT_TOKEN", withAccess: .accessibleAfterFirstUnlock) + keychain.set(refresh, forKey: "REFRESH_TOKEN", withAccess: .accessibleAfterFirstUnlock) + completion(response, nil) + } + } else { + AuthenticationAPI.issueJWTTokenAPI { (response, error) in + + guard let jwt = response?.token, + let refresh = response?.refreshToken, + error == nil else { + + // set corrupted + UserDefaults.standard.set(true, forKey: "ReauthenticationNeededKey") + completion(response, .TokenExpiredError) + return + } + DLog("Authentication API: JWT and refresh tokens updated. \(jwt)") + + UserDefaults.standard.set(false, forKey: "ReauthenticationNeededKey") + keychain.set(jwt, forKey: "JWT_TOKEN", withAccess: .accessibleAfterFirstUnlock) + keychain.set(refresh, forKey: "REFRESH_TOKEN", withAccess: .accessibleAfterFirstUnlock) + + completion(response, nil) + + } + } + } + + +} + +struct AuthenticationToken { + var token: String + + func getSubject() -> String? { + let sections = token.split(separator: ".") + + guard sections.count >= 2 else { return nil } + + // we may want to iterate over all 3 substrings + var sectionOfInterest = String(sections[1]) + + // add filler characters if not present + if (sectionOfInterest.count % 4 > 0){ + sectionOfInterest += String(repeating: "=", count: 4 - (sectionOfInterest.count % 4)) + } + + if let decodedData = Data(base64Encoded: sectionOfInterest) { + let dictionary: [String: Any]? = try? JSONSerialization.jsonObject(with: decodedData, options: []) as? [String: Any] + + if let subject = dictionary?["sub"] as? String { + return subject + } + } + + return nil + } + + func getExpiry() -> Date? { + let sections = token.split(separator: ".") + + guard sections.count >= 2 else { return nil } + + // we may want to iterate over all 3 substrings + var sectionOfInterest = String(sections[1]) + + // add filler characters if not present + if (sectionOfInterest.count % 4 > 0){ + sectionOfInterest += String(repeating: "=", count: 4 - (sectionOfInterest.count % 4)) + } + + if let decodedData = Data(base64Encoded: sectionOfInterest) { + let dictionary: [String: Any]? = try? JSONSerialization.jsonObject(with: decodedData, options: []) as? [String: Any] + + if let expiry = dictionary?["exp"] as? Double { + return Date(timeIntervalSince1970: expiry) + } + } + + return nil + } +} diff --git a/CovidSafe/API/ChangePostcodeAPI.swift b/CovidSafe/API/ChangePostcodeAPI.swift index d5bc490..72a79e6 100644 --- a/CovidSafe/API/ChangePostcodeAPI.swift +++ b/CovidSafe/API/ChangePostcodeAPI.swift @@ -17,11 +17,6 @@ class ChangePostcodeAPI: CovidSafeAuthenticatedAPI { return } - guard let headers = try? authenticatedHeaders() else { - completion(.TokenExpiredError) - return - } - let params = [ "postcode": newPostcode, ] @@ -29,7 +24,7 @@ class ChangePostcodeAPI: CovidSafeAuthenticatedAPI { method: .post, parameters: params, encoding: JSONEncoding.default, - headers: headers, + headers: authenticatedHeaders, interceptor: CovidRequestRetrier(retries:3)).validate().responseDecodable(of: DeviceResponse.self) { (response) in switch response.result { case .success: diff --git a/CovidSafe/API/CovidNetworking.swift b/CovidSafe/API/CovidNetworking.swift index 06c760d..e18cef6 100644 --- a/CovidSafe/API/CovidNetworking.swift +++ b/CovidSafe/API/CovidNetworking.swift @@ -61,16 +61,20 @@ enum CovidSafeAPIError: Error { class CovidSafeAuthenticatedAPI { - static func authenticatedHeaders() throws -> HTTPHeaders? { - let keychain = KeychainSwift() - - guard let token = keychain.get("JWT_TOKEN") else { - throw CovidSafeAPIError.TokenExpiredError + static var isBusy = false + + static var authenticatedHeaders: HTTPHeaders { + get { + let keychain = KeychainSwift() + + guard let token = keychain.get("JWT_TOKEN") else { + return [] + } + let headers: HTTPHeaders = [ + "Authorization": "Bearer \(token)" + ] + return headers } - let headers: HTTPHeaders = [ - "Authorization": "Bearer \(token)" - ] - return headers } static func processUnauthorizedError(_ data: Data) -> CovidSafeAPIError { diff --git a/CovidSafe/API/CovidRequestRetrier.swift b/CovidSafe/API/CovidRequestRetrier.swift index aa30e59..b038cbb 100644 --- a/CovidSafe/API/CovidRequestRetrier.swift +++ b/CovidSafe/API/CovidRequestRetrier.swift @@ -7,16 +7,71 @@ import Foundation import Alamofire +import KeychainSwift + final class CovidRequestRetrier: Alamofire.RequestInterceptor { private let numRetries: Int private var retriesExecuted: Int = 0 + private var triedRefresh = false init(retries: Int) { self.numRetries = retries } + + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + var urlRequest = urlRequest + let keychain = KeychainSwift() + let refreshExists = keychain.get("REFRESH_TOKEN") != nil + // prevent authenticated api calls if the re-registration flow has been started + if UserDefaults.standard.bool(forKey: "ReauthenticationNeededKey") && + refreshExists { + completion(.failure(CovidSafeAPIError.TokenExpiredError)) + return + } + + // check headers an update if needed. + // intercept the first call to the API after app updates to retrieve new tokens + if !refreshExists && + keychain.get("JWT_TOKEN") != nil { + AuthenticationAPI.issueTokensAPI { (response, error) in + guard let token = response?.token else { + completion(.success(urlRequest)) + return + } + // update the token + urlRequest.headers.add(name: "Authorization", value: "Bearer \(token)") + completion(.success(urlRequest)) + } + return + } + + guard let token = keychain.get("JWT_TOKEN"), + urlRequest.headers["Authorization"] != nil else { + completion(.success(urlRequest)) + return + } + + // update the token in case is was updated in a retry + urlRequest.headers.add(name: "Authorization", value: "Bearer \(token)") + completion(.success(urlRequest)) + } func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { - guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 403 else { + + guard retriesExecuted < numRetries else { + completion(.doNotRetryWithError(error)) + return + } + + if let covidError = error.asAFError?.underlyingError as? CovidSafeAPIError, covidError == .TokenExpiredError { + retriesExecuted = numRetries + // for some reason the retry is getting called even after doNotRetryWithError below. + // set retries to max and the guard above stops it all + completion(.doNotRetryWithError(error)) + return + } + + guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 403 || response.statusCode == 401 else { /// The request did not fail due to a 403 Forbidden response. let isServerTrustEvaluationError = error.asAFError?.isServerTrustEvaluationError ?? false if ( retriesExecuted >= numRetries || isServerTrustEvaluationError) { @@ -25,6 +80,21 @@ final class CovidRequestRetrier: Alamofire.RequestInterceptor { retriesExecuted += 1 return completion(.retryWithDelay(1.0)) } + + if !triedRefresh && + (response.statusCode == 403 || response.statusCode == 401) { + triedRefresh = true + retriesExecuted += 1 + AuthenticationAPI.issueTokensAPI { (response, authError) in + // this will update the tokens automatically + guard let respError = authError, respError == .TokenExpiredError else { + completion(.doNotRetryWithError(error)) + return + } + completion(.retryWithDelay(1.0)) + } + return + } return completion(.doNotRetryWithError(error)) } } diff --git a/CovidSafe/API/GetTempIdAPI.swift b/CovidSafe/API/GetTempIdAPI.swift index c058abe..3020484 100644 --- a/CovidSafe/API/GetTempIdAPI.swift +++ b/CovidSafe/API/GetTempIdAPI.swift @@ -13,21 +13,28 @@ class GetTempIdAPI: CovidSafeAuthenticatedAPI { private static let apiVersion = 2 static func getTempId(completion: @escaping (String?, Int?, Swift.Error?, CovidSafeAPIError?) -> Void) { + guard isBusy == false else { + completion(nil ,nil ,nil, .UnknownError) + return + } + guard let apiHost = PlistHelper.getvalueFromInfoPlist(withKey: "API_Host", plistName: "CovidSafe-config") else { return } - guard let headers = try? authenticatedHeaders() else { - completion(nil, nil, nil, .TokenExpiredError) - return - } let params = [ "version" : apiVersion ] + + guard authenticatedHeaders.count > 0 else { + completion(nil, nil, nil, .TokenExpiredError) + return + } + CovidNetworking.shared.session.request("\(apiHost)/getTempId", method: .get, parameters: params, - headers: headers, + headers: authenticatedHeaders, interceptor: CovidRequestRetrier(retries: 3)).validate().responseDecodable(of: TempIdResponse.self) { (response) in switch response.result { case .success: diff --git a/CovidSafe/API/InitiateUploadAPI.swift b/CovidSafe/API/InitiateUploadAPI.swift index 146358a..0e157c9 100644 --- a/CovidSafe/API/InitiateUploadAPI.swift +++ b/CovidSafe/API/InitiateUploadAPI.swift @@ -17,7 +17,11 @@ class InitiateUploadAPI { let headers: HTTPHeaders = [ "Authorization": "Bearer \(session)" ] - CovidNetworking.shared.session.request("\(apiHost)/requestUploadOtp", method: .get, headers: headers).validate().responseString { (response) in + CovidNetworking.shared.session.request("\(apiHost)/requestUploadOtp", + method: .get, + headers: headers, + interceptor: CovidRequestRetrier(retries: 3) + ).validate().responseString { (response) in switch response.result { case .success: if response.value != nil { diff --git a/CovidSafe/API/MessageAPI.swift b/CovidSafe/API/MessageAPI.swift index cd0974d..82867e3 100644 --- a/CovidSafe/API/MessageAPI.swift +++ b/CovidSafe/API/MessageAPI.swift @@ -55,6 +55,10 @@ class MessageAPI: CovidSafeAuthenticatedAPI { } private static func shouldGetMessages() -> Bool { + guard isBusy == false else { + return false + } + let lastChecked = UserDefaults.standard.double(forKey: keyLastApiUpdate) let versionChecked = UserDefaults.standard.integer(forKey: keyLastVersionChecked) @@ -87,11 +91,6 @@ class MessageAPI: CovidSafeAuthenticatedAPI { return } - guard let headers = try? authenticatedHeaders() else { - completion(nil, .TokenExpiredError) - return - } - let preferredLanguages = Locale.preferredLanguages.count > 5 ? Locale.preferredLanguages[0...5].joined(separator: ",") : Locale.preferredLanguages.joined(separator: ",") var params: [String : Any] = [ @@ -107,10 +106,14 @@ class MessageAPI: CovidSafeAuthenticatedAPI { if let remoteToken = msgRequest.remotePushToken { params["token"] = remoteToken } + + isBusy = true + CovidNetworking.shared.session.request("\(apiHost)/messages", method: .get, parameters: params, - headers: headers + headers: authenticatedHeaders, + interceptor: CovidRequestRetrier(retries: 3) ).validate().responseDecodable(of: MessageResponse.self) { (response) in switch response.result { case .success: @@ -127,6 +130,7 @@ class MessageAPI: CovidSafeAuthenticatedAPI { } UserDefaults.standard.set(Bundle.main.version, forKey: keyLastVersionChecked) + isBusy = false completion(messageResponse, nil) case .failure(_): guard let statusCode = response.response?.statusCode else { @@ -147,6 +151,8 @@ class MessageAPI: CovidSafeAuthenticatedAPI { completion(nil, .RequestError) return } + + isBusy = false completion(nil, .ServerError) } } diff --git a/CovidSafe/API/PhoneValidationAPI.swift b/CovidSafe/API/PhoneValidationAPI.swift index 97a45da..4088050 100644 --- a/CovidSafe/API/PhoneValidationAPI.swift +++ b/CovidSafe/API/PhoneValidationAPI.swift @@ -28,8 +28,7 @@ class PhoneValidationAPI { CovidNetworking.shared.session.request("\(apiHost)/initiateAuth", method: .post, parameters: params, - encoding: JSONEncoding.default, - interceptor: CovidRequestRetrier(retries:3)).validate().responseDecodable(of: AuthResponse.self) { (response) in + encoding: JSONEncoding.default).validate().responseDecodable(of: AuthResponse.self) { (response) in switch response.result { case .success: guard let authResponse = response.value else { return } diff --git a/CovidSafe/API/RespondToAuthChallengeAPI.swift b/CovidSafe/API/RespondToAuthChallengeAPI.swift index b4435aa..eca74db 100644 --- a/CovidSafe/API/RespondToAuthChallengeAPI.swift +++ b/CovidSafe/API/RespondToAuthChallengeAPI.swift @@ -12,7 +12,7 @@ class RespondToAuthChallengeAPI { static func respondToAuthChallenge(session: String, code: String, - completion: @escaping (String?, ChallengeErrorResponse?) -> Void) { + completion: @escaping (ChallengeResponse?, ChallengeErrorResponse?) -> Void) { guard let apiHost = PlistHelper.getvalueFromInfoPlist(withKey: "API_Host", plistName: "CovidSafe-config") else { return } @@ -21,11 +21,11 @@ class RespondToAuthChallengeAPI { "code": code ] - CovidNetworking.shared.session.request("\(apiHost)/respondToAuthChallenge", method: .post, parameters: params, encoding: JSONEncoding.default).validate().responseDecodable(of: ChallengeResponse.self) { (response) in + CovidNetworking.shared.session.request("\(apiHost)/v2/respondToAuthChallenge", method: .post, parameters: params, encoding: JSONEncoding.default).validate().responseDecodable(of: ChallengeResponse.self) { (response) in switch response.result { case .success: guard let challengeResponse = response.value else { return } - completion(challengeResponse.token, nil) + completion(challengeResponse, nil) case .failure(_): guard let errorData = response.data else { completion(nil, nil) @@ -55,9 +55,11 @@ struct ChallengeErrorResponse: Decodable, Error { } struct ChallengeResponse: Decodable { - let token: String - - enum CodingKeys: String, CodingKey { - case token - } + let token: String + let refreshToken: String? + + enum CodingKeys: String, CodingKey { + case token + case refreshToken + } } diff --git a/CovidSafe/API/RestrictionsAPI.swift b/CovidSafe/API/RestrictionsAPI.swift index 39d3096..0c7000e 100644 --- a/CovidSafe/API/RestrictionsAPI.swift +++ b/CovidSafe/API/RestrictionsAPI.swift @@ -22,17 +22,13 @@ class RestrictionsAPI: CovidSafeAuthenticatedAPI { return } - guard let headers = try? authenticatedHeaders() else { - completion(nil, .TokenExpiredError) - return - } - let params = ["state": "\(forState.rawValue.lowercased())"] CovidNetworking.shared.session.request("\(apiHost)/restrictions", method: .get, parameters: params, - headers: headers + headers: authenticatedHeaders, + interceptor: CovidRequestRetrier(retries: 3) ).validate().responseDecodable(of: StateRestriction.self) { (response) in switch response.result { case .success: diff --git a/CovidSafe/API/StatisticsAPI.swift b/CovidSafe/API/StatisticsAPI.swift index 9b0fd48..4f8a2d7 100644 --- a/CovidSafe/API/StatisticsAPI.swift +++ b/CovidSafe/API/StatisticsAPI.swift @@ -18,17 +18,13 @@ class StatisticsAPI: CovidSafeAuthenticatedAPI { return } - guard let headers = try? authenticatedHeaders() else { - completion(nil, .TokenExpiredError) - return - } - let parameters = ["state" : "\(forState.rawValue)"] CovidNetworking.shared.session.request("\(apiHost)/v2/statistics", method: .get, parameters: parameters, - headers: headers + headers: authenticatedHeaders, + interceptor: CovidRequestRetrier(retries: 3) ).validate().responseDecodable(of: StatisticsResponse.self) { (response) in switch response.result { case .success: diff --git a/CovidSafe/CovidStatisticsViewController.swift b/CovidSafe/CovidStatisticsViewController.swift index f7a8679..de94a40 100644 --- a/CovidSafe/CovidStatisticsViewController.swift +++ b/CovidSafe/CovidStatisticsViewController.swift @@ -460,6 +460,7 @@ extension CovidStatisticsViewController: StatisticsTableDelegate { func getStateValues() -> [StateTerritory] { return [StateTerritory.ACT, StateTerritory.NSW, + StateTerritory.NT, StateTerritory.QLD, StateTerritory.SA, StateTerritory.TAS, diff --git a/CovidSafe/Debug.storyboard b/CovidSafe/Debug.storyboard index c17121d..d85488b 100644 --- a/CovidSafe/Debug.storyboard +++ b/CovidSafe/Debug.storyboard @@ -46,21 +46,21 @@ - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - +