COVIDSafe code from version 2.4 (#47)

This commit is contained in:
COVIDSafe Support 2021-03-18 14:16:35 +11:00 committed by covidsafe-support
parent e577a9e7aa
commit 1478088e19
40 changed files with 1125 additions and 657 deletions

View file

@ -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
}
}

View file

@ -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:

View file

@ -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 {

View file

@ -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<URLRequest, Error>) -> 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))
}
}

View file

@ -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:

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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 }

View file

@ -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
}
}

View file

@ -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:

View file

@ -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: