// // 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 } guard let authHeaders = try? authenticatedHeaders() else { completion(nil, .RequestError) return } CovidNetworking.shared.session.request("\(apiHost)/issueInitialRefreshToken", method: .post, encoding: JSONEncoding.default, headers: authHeaders ).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.shared 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.shared // 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 { let refreshTokenBeforeRefresh = keychain.get("REFRESH_TOKEN") AuthenticationAPI.issueJWTTokenAPI { (response, error) in guard let jwt = response?.token, let refresh = response?.refreshToken, error == nil else { // if the token in the system before the call is different than the one in keychain, this is a concurrency issue, the token was refresh already and that is why this failed. We should not start re-authentication guard refreshTokenBeforeRefresh == keychain.get("REFRESH_TOKEN") else { completion(response, .TokenAlreadyRefreshedError) return } // set corrupted only when it has not been changed 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 } }