Centralized token for concurrent asynchronous network tasks - ios

I have an application that is pulling API data for multiple tasks within itself.
The main API data request is to pull the appointments for a given provider or location in our health system.
To have the proper keys to match the provider ID or location ID to the actual name, I pull that data from the API as well to make sure they are up to date.
I have it working so the network call checks if the current API token is active, and if not, automatically refreshes it and re-calls the network data. Once the token is confirmed to either be currently active OR to be refreshed properly, it returns true.
However, the 3 different network pulls are calling data asynchronously and one will update the API token while the other is attempting to do the same and then the API token gets updated repeatedly within the same sequence unnecessarily.
To fix this, I am attempting to switch to a localized token function which checks if the current token is active, and if not refreshes it. It also checks if there is a token update already in progress, and if so stops the other functions from updating it as well. It also re-calls the original function in order to wait for the updated token to appear.
The problem I'm running into is I seem to have created a loop that crashes my app before the new token can appear. Essentially what I'm guessing I need to solve this is to wait for a response before re-calling the loop, but I'm unsure on the best way to do this.
Here is my token check/refresh code:
My model also has a var tokenUpdateInProgress = false so that the new function can alert when a new update begins.
func isTokenActiveAndRefreshIfNot(_ completion: #escaping (Bool) -> ()) {
print("running tokenActive function")
var returnBool = false
print("tokenUpdateInProgressz: \(tokenUpdateInProgress)")
if tokenUpdateInProgress == false {
print("no other update in progress, start the check here")
tokenUpdateInProgress = true
let resourceListURL = "https://api.carecloud.com/v2/oauth2/token_info"
var request = URLRequest(url:URL(string:resourceListURL)!)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
var json = JSON()
URLSession.shared.dataTask(with: request){ data, response, error in
print("data pull called in isTokenActive")
if let httpResponse = response as? HTTPURLResponse {
print("status code: \(httpResponse.statusCode)")
httpStatusCode = httpResponse.statusCode
}
if httpStatusCode == 200 {
print("token is current, proceed")
tokenUpdateInProgress = false
returnBool = true
json = try! JSON(data: data!)
for (_, _) in json {
if let expiration = json["expires_in"].string {
if let double = Double(expiration) {
let dateTime = Date()
let newDateTime = dateTime.addingTimeInterval(double)
tokenExpiresDateTime = newDateTime
}
}
}
}
if httpStatusCode == 403 || httpStatusCode == 401 {
let downloadGroup = DispatchGroup()
print("token is expired, need to refresh it")
tokenUpdate = false
downloadGroup.enter()
refreshToken({ (tokenRefreshed) in. //this function refreshes my token successfully
if tokenRefreshed == true {
//can do something here if token refresh works correctly
returnBool = true
tokenUpdateInProgress = false
downloadGroup.leave()
downloadGroup.notify(queue: DispatchQueue.main) {
// completion(returnItem)
}
}
})
}
completion(returnBool)
}.resume()
} else if tokenUpdateInProgress == true {
print("token update in progress, re-run check and wait for result")
isTokenActiveAndRefreshIfNot { (result) in //this creates a loop if network not working or token refresh not working for any reason. This loops too fast and leads to a crash.
returnBool = result
}
completion(returnBool)
}
}
The three functions all pulling this data are:
1:
func monthSchedule(resourceID: String, locationID: String, page: Int, _ completion: #escaping ([appt]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenUpdated) in
print("token update within monthschedule: \(tokenUpdated)")
}
... runs network pull/API interpretation from token once confirmed ok
}
2:
func pullLocationsList(_ completion: #escaping ([Int: String]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenReady) in
print("token ready for pull location list: \(tokenReady)")
}
... runs network pull/API interpretation from token once confirmed ok
}
3:
func pullResourceList(_ completion: #escaping ([Int: String]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenReady) in
print("token ready for pull resource list: \(tokenReady)")
}
... runs network pull/API interpretation from token once confirmed ok
}

Related

Chaining API calls

I've built myself an APIService class, handling all interaction with APIs my App is using
func callEndpointX(token: String, completion: #escaping(Result<User, APIError>) -> Void) {
guard let endpoint = URL(string: apiBaseUrl + "/endpointX") else {fatalError()}
var request = URLRequest(url: endpoint)
request.addValue("Bearer " + token, forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let jsonData = data
else { ... completion(.failure(.responseError)); return }
do {
let response = try JSONDecoder()...
completion(.success(response.detailresponse!))
} catch {
...
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
My challenge is that the Bearer Tokens I'm passing along do expire. What I would like to do in case of an expired token is to silently refresh the token for the user before hitting the api.
So I've got a mini function func tokenExpired() -> Bool {...} checking if the Token has alreaady expired and I have access to another API all refreshToken in a similar way as func callEndpointX above.
What I'm struggling with is how to chain these together to avoid race conditions - i.e. when func callEndpointX gets called, it should check func tokenExpired() and not continue any other work until that's done. If expired, it should first execute refreshToken and only after that returns continue with finally executing the ret of func callEndpointX.
All this chaining is messing with my brain and I was hoping someone would be able to guide me here.
Many thanks!!!
A solution is to call recursively callEndPointX after token refresh is completed.
Assume your token request is like this:
func requestToken(completion:#escaping((String) -> Void))
then
func callEndpointX(token: String, completion: #escaping(Result<User, APIError>) -> Void) {
if tokenExpired() {
requestToken(completion: { newToken in
callEndpointX(token: newToken, completion: completion)
})
}
else {
// your code for callendpointX
}
}

Refresh access token with URLSession after getting a 401 response code & retry request

I'm working on building a networking client for my iOS application which uses OAuth 2.0 Authorization techniques (Access & Refresh Token). There is a feature for my networking client that I have been struggling to implement:
When a 401 error occurs that means the Access Token has expired and I need to send a Refresh Token over to my server to obtain a new Access Token.
After getting a new Access Token I need to redo the previous request that got the 401 error.
So far I have written this code for my networking client:
typealias NetworkCompletion = Result<(Data, URLResponse), FRNetworkingError>
/// I am using a custom result type to support just an Error and not a Type object for success
enum NetworkResponseResult<Error> {
case success
case failure(Error)
}
class FRNetworking: FRNetworkingProtocol {
fileprivate func handleNetworkResponse(_ response: HTTPURLResponse) -> NetworkResponseResult<Error> {
switch response.statusCode {
case 200...299: return .success
case 401: return .failure(FRNetworkingError.invalidAuthToken)
case 403: return .failure(FRNetworkingError.forbidden)
case 404...500: return .failure(FRNetworkingError.authenticationError)
case 501...599: return .failure(FRNetworkingError.badRequest)
default: return .failure(FRNetworkingError.requestFailed)
}
}
func request(using session: URLSession = URLSession.shared, _ endpoint: Endpoint, completion: #escaping(NetworkCompletion) -> Void) {
do {
try session.dataTask(with: endpoint.request(), completionHandler: { (data, response, error) in
if let error = error {
print("Unable to request data \(error)")
// Invoke completion for error
completion(.failure(.unknownError))
} else if let data = data, let response = response {
// Passing Data and Response into completion for parsing in ViewModels
completion(.success((data, response)))
}
}).resume()
} catch {
print("Failed to execute request", error)
completion(.failure(.requestFailed))
}
}
}
Endpoint is just a struct that builds a URLRequest:
struct Endpoint {
let path: String
let method: HTTPMethod
let parameters: Parameters?
let queryItems: [URLQueryItem]?
let requiresAuthentication: Bool
var url: URL? {
var components = URLComponents()
components.scheme = "http"
components.host = "127.0.0.1"
components.port = 8000
components.path = "/api\(path)"
components.queryItems = queryItems
return components.url
}
func request() throws -> URLRequest {
/// Creates a request based on the variables per struct
}
}
Where do I put the code that allows the FRNetworking.request() to get a new token and retry the request?
I have done the following inside the else if let data = data, let response = response statement:
if let response = response as? HTTPURLResponse {
let result = self.handleNetworkResponse(response)
switch result {
case .failure(FRNetworkingError.invalidAuthToken):
break
// TODO: Get new Access Token and refresh?
default:
break
}
}
Is this the right approach to refresh the token and redo the API call or is there a better way?
You have to write a function that updates the token and, depending on the result, returns true or false
private func refreshAccessToken(completion: #escaping (Bool) -> Void {
// Make a request to refresh the access token
// Update the accessToken and refreshToken variables when the request is completed
// Call completion(true) if the request was successful, completion(false) otherwise
}
Declare 2 variables at the beginning of the class
var session: URLSession
var endpoint: Endpoint
Inside the case .failure assign these variables
session = session
endpoint = endpoint
Then call refreshAccessToken method. The final code will look like this
if let response = response as? HTTPURLResponse {
let result = self.handleNetworkResponse(response)
switch result {
case .failure(FRNetworkingError.invalidAuthToken):
session = session
endpoint = endpoint
self?.refreshAccessToken { success in
if success {
self?.request(using: session, endpoint, completion: completion)
} else {
completion(.failure(.unknownError))
}
}
break
default:
break
}
}

iOS rxSwift: retryWhen updating refresh token

I have a static function calling a network service.
When the 400 response code happens I would like to redo the network call.
The current code is working, except that the refreshToken in the header does not update between one try and another.
I think that the problem is because the Observable created but the request function does not update at the retry.
I rode on the web that I should use a deferred method on the Observable, but I don't know how.
I've tried moving the code: headers = [HeaderKeys.refreshToken.rawValue: "test test"] anywhere but still it never makes a call with the "test test" refresh token. it always uses the old one.
How can I fix this?
static func getAccessToken() -> Observable<GetAccessTokenResponse> {
var retryCounter = 0
let maxRetryCounter = 3
let delayRetry = 10.0
guard let refreshToken = NetworkHelper.shared.refreshToken else {
return Observable.error(AuthenticationError.networkError)
}
var headers = [HeaderKeys.refreshToken.rawValue: refreshToken]
return NetworkHelper.shared
.request(url: CoreAPI.accessToken.url, request: nil, headers: headers, responseType: GetAccessTokenResponse.self, method: .get, encoding: nil)
.catchError({ (error) -> Observable<(GetAccessTokenResponse?, Int)> in
return Observable.error(AuthenticationError.networkError)
})
.flatMap({ (response) -> Observable<GetAccessTokenResponse> in
// check http status code
switch response.1 {
case 200:
guard response.0?.accessToken != nil else {
return Observable.error(AuthenticationError.genericError)
}
// success
return Observable.just(response.0!)
case 400:
// invalid parameters, refresh token not existing
return Observable.error(AuthenticationError.invalidParameters)
case 404:
// user not existing
return Observable.error(AuthenticationError.userDoesntExist)
default:
// by default return network error
return Observable.error(AuthenticationError.networkError)
}
})
.retryWhen({ (errors) -> Observable<Void> in
return errors
.do(onNext: { (error) in
headers = [HeaderKeys.refreshToken.rawValue: "test test"]
})
.flatMap({error -> Observable<Int> in
debugLog("Retrying get refresh token")
if retryCounter >= maxRetryCounter {
let authError = error as? AuthenticationError ?? .genericError
if authError == AuthenticationError.invalidParameters {
// publish logged false on subject
VDAAuthenticationManager.shared.logged.onNext(false)
}
return Observable.error(error)
}
// increase the retry counter and retry
retryCounter += 1
return Observable<Int>.timer(delayRetry, scheduler: MainScheduler.instance)
})
.flatMap ({ (_) -> Observable<Void> in
return Observable.just(())
})
})
}
In the article RxSwift and Retrying a Network Request Despite Having an Invalid Token I explain how to keep and update a token and how to handle retries when you get a 401 error. Using deferred is part of the answer.
In your particular case. It looks like you could use my service like this:
func getToken(lastResponse: GetAccessTokenResponse?) -> Observable<(response: HTTPURLResponse, data: Data)> {
guard let refreshToken = lastResponse?.refreshToken else { return Observable.error(AuthenticationError.networkError) }
var request = URLRequest(url: CoreAPI.accessToken.url)
request.addValue(refreshToken, forHTTPHeaderField: HeaderKeys.refreshToken.rawValue)
return URLSession.shared.rx.response(request: request)
}
func extractToken(data: Data) throws -> GetAccessTokenResponse {
return try JSONDecoder().decode(GetAccessTokenResponse.self, from: data)
}
let tokenService = TokenAcquisitionService(initialToken: nil, getToken: getToken, extractToken: extractToken(data:))
In the above, you will have to pass a valid initialToken instead of nil or you will have to modify the getToken so it can get a token even if it doesn't have a refresh token.
An example of how to use deferred is below:
let response = Observable
.deferred { tokenAcquisitionService.token.take(1) }
.flatMap { makeRequest(withToken: $0) }
.map { response in
guard response.response.statusCode != 401 else { throw ResponseError.unauthorized }
return response
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
I explain in the article what each line of code is for and how it works.

Delay task until completed

It is a very common question for people to ask "How do I delay a function or a chunk of code?" but that is not what I need here.
I need my code to wait until a certain task is complete, otherwise my function receives an error that I have no access_token (as the code doesn't wait for fetching the data from the Spotify server).
Here is my code so far, with the attempt to add a DispatchGroup:
func getAccessToken() throws -> Spotify.JSONStandard {
var accessToken: Spotify.JSONStandard!
let group = DispatchGroup() // <- Create group
group.enter() // <- Enter group
Alamofire.request("https://accounts.spotify.com/api/token", method: .post, parameters: spotify.parameters, headers: nil).responseJSON(completionHandler: {
response in
// Check if response is valid
if let newValue = response.result.value as? Spotify.JSONStandard {
accessToken = newValue
}
group.leave() // <- Leave group
})
group.wait() // <- Wait until task is completed
// \/ Delay this code until complete \/
if accessToken != nil {
return accessToken
}
else {
throw SpotifyError.failedToGetAccessToken
}
// /\ /\
}
Without the groups, my code throws SpotifyError.failedToGetAccessToken (the access_token is nil).
However, after adding the groups, my code just hangs and waits forever. How can I delay this code from being completed?
I know getting the token has no issues, as if I remove the return and place a print within the request, I get my expected result.
If you have any questions, please ask
Don't try to make an asynchronous task synchronous
This is a solution with a completion handler and a custom enum for convenience
enum Result {
case success(Spotify.JSONStandard), failure(Error)
}
func getAccessToken(completion: #escaping (Result)->()) {
Alamofire.request("https://accounts.spotify.com/api/token", method: .post, parameters: spotify.parameters, headers: nil).responseJSON(completionHandler: {
response in
// Check if response is valid
if let newValue = response.result.value as? Spotify.JSONStandard {
completion(.success(newValue)
} else {
completion(.failure(SpotifyError.failedToGetAccessToken))
}
})
}
and call it
getAccessToken { result in
switch result {
case .success(let token) : // do something with the token
case .failure(let error) : // do something with the error
}
}

Alamofire : How to handle errors globally

My question is quite similar to this one, but for Alamofire : AFNetworking: Handle error globally and repeat request
How to be able to catch globally an error (typically a 401) and handle it before other requests are made (and eventually failed if not managed) ?
I was thinking of chaining a custom response handler, but that's silly to do it on each request of the app.
Maybe subclassing, but which class should i subclass to handle that ?
Handling refresh for 401 responses in an oauth flow is quite complicated given the parallel nature of NSURLSessions. I have spent quite some time building an internal solution that has worked extremely well for us. The following is a very high level extraction of the general idea of how it was implemented.
import Foundation
import Alamofire
public class AuthorizationManager: Manager {
public typealias NetworkSuccessHandler = (AnyObject?) -> Void
public typealias NetworkFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError) -> Void
private typealias CachedTask = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void
private var cachedTasks = Array<CachedTask>()
private var isRefreshing = false
public func startRequest(
method method: Alamofire.Method,
URLString: URLStringConvertible,
parameters: [String: AnyObject]?,
encoding: ParameterEncoding,
success: NetworkSuccessHandler?,
failure: NetworkFailureHandler?) -> Request?
{
let cachedTask: CachedTask = { [weak self] URLResponse, data, error in
guard let strongSelf = self else { return }
if let error = error {
failure?(URLResponse, data, error)
} else {
strongSelf.startRequest(
method: method,
URLString: URLString,
parameters: parameters,
encoding: encoding,
success: success,
failure: failure
)
}
}
if self.isRefreshing {
self.cachedTasks.append(cachedTask)
return nil
}
// Append your auth tokens here to your parameters
let request = self.request(method, URLString, parameters: parameters, encoding: encoding)
request.response { [weak self] request, response, data, error in
guard let strongSelf = self else { return }
if let response = response where response.statusCode == 401 {
strongSelf.cachedTasks.append(cachedTask)
strongSelf.refreshTokens()
return
}
if let error = error {
failure?(response, data, error)
} else {
success?(data)
}
}
return request
}
func refreshTokens() {
self.isRefreshing = true
// Make the refresh call and run the following in the success closure to restart the cached tasks
let cachedTaskCopy = self.cachedTasks
self.cachedTasks.removeAll()
cachedTaskCopy.map { $0(nil, nil, nil) }
self.isRefreshing = false
}
}
The most important thing here to remember is that you don't want to run a refresh call for every 401 that comes back. A large number of requests can be racing at the same time. Therefore, you want to act on the first 401, and queue all the additional requests until the 401 has succeeded. The solution I outlined above does exactly that. Any data task that is started through the startRequest method will automatically get refreshed if it hits a 401.
Some other important things to note here that are not accounted for in this very simplified example are:
Thread-safety
Guaranteed success or failure closure calls
Storing and fetching the oauth tokens
Parsing the response
Casting the parsed response to the appropriate type (generics)
Hopefully this helps shed some light.
Update
We have now released 🔥🔥 Alamofire 4.0 🔥🔥 which adds the RequestAdapter and RequestRetrier protocols allowing you to easily build your own authentication system regardless of the authorization implementation details! For more information, please refer to our README which has a complete example of how you could implement on OAuth2 system into your app.
Full Disclosure: The example in the README is only meant to be used as an example. Please please please do NOT just go and copy-paste the code into a production application.
in Alamofire 5 you can use RequestInterceptor
Here is my error handling for 401 error in one of my projects, every requests that I pass the EnvironmentInterceptor to it the func of retry will be called if the request get to error
and also the adapt func can help you to add default value to your requests
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = KeychainWrapper.standard.string(forKey: KeychainsKeys.token.rawValue) else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authentication.rawValue)
completion(.success(adaptedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
//get token
guard let refreshToken = KeychainWrapper.standard.string(forKey: KeychainsKeys.refreshToken.rawValue) else {
completion(.doNotRetryWithError(error))
return
}
APIDriverAcountClient.refreshToken(refreshToken: refreshToken) { res in
switch res {
case .success(let response):
let saveAccessToken: Bool = KeychainWrapper.standard.set(response.accessToken, forKey: KeychainsKeys.token.rawValue)
let saveRefreshToken: Bool = KeychainWrapper.standard.set(response.refreshToken, forKey: KeychainsKeys.refreshToken.rawValue)
let saveUserId: Bool = KeychainWrapper.standard.set(response.userId, forKey: KeychainsKeys.uId.rawValue)
print("is accesstoken saved ?: \(saveAccessToken)")
print("is refreshToken saved ?: \(saveRefreshToken)")
print("is userID saved ?: \(saveUserId)")
completion(.retry)
break
case .failure(let err):
//TODO logout
break
}
}
} else {
completion(.doNotRetry)
}
}
and you can use it like this :
#discardableResult
private static func performRequest<T: Decodable>(route: ApiDriverTrip, decoder: JSONDecoder = JSONDecoder(), completion: #escaping (AFResult<T>)->Void) -> DataRequest {
return AF.request(route, interceptor: EnvironmentInterceptor())
.responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
completion(response.result)
}

Resources