Jun 29, 2019

Handling expired OAuth tokens in an concurrent request flow

When setting up a system that handles concurrent OAuth requests, it is important to handle expired tokens gracefully, to not interrupt the users workflow. The user should not notice any token fetches or refreshes happening, nor should expired tokens lead to to any error messages exposed to the user.

To see how this can achieved, let‘s first define how the token structure is defined. For this example I will be using an OAuth2 token:

struct OAuth2Token: Codable {
    
  let date = Date() // date when the token was initialized
  var accessToken: String // access token
  var refreshToken: String? // refresh token (optional)
  var expiresIn: Int // seconds until token expires
  var tokenType: String // for example "Bearer"
    
  // returns if token is still valid or has expired
  var isValid: Bool {
    let now = Date()
    let seconds = TimeInterval(expiresIn)
    return now.timeIntervalSince(date) < seconds
  }
}

Typically an OAuth2 token will look very similar to this.

Before an authorized URLRequest can be sent, it must be constructed by adding a token to it‘s header. Otherwise it will fail. So before the very first URLRequest in our apps life-cycle is sent, we need to fetch a token from the API provider.

We do this by checking for a token first before constructing the URLRequest. If we have a token it must also be valid, otherwise it needs to refreshed. This is also something the API provider needs to do. If the token is valid, then we can construct the request and pass it on the the URLSession and begin the URLSessionDataTask.

private var token: OAuth2Token?

func request(url: URL, completion: @escaping (Result<Any, Error>) -> Void) {
  // check if a token is available
  if let token = token {
    // token found -> check if token is valid
    if token.isValid == false {
      // check if refresh token is available
      if let token = token.refreshToken {
        // refresh current token
        refreshToken(tokenString: token)
      } else {
        // fetch new token
        fetchToken()
      }
    } else {
      // token is valid so now setup url request 
      var request = URLRequest(url: url)
      request.setValue(token.tokenType + " " + token.accessToken, forHTTPHeaderField: "Authorization")
      // and data task
      let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        // handle the result of the request however you like
      }
      // begin task
      task.resume()
    }
  } else {
    // fetch new OAuth2Token
    fetchToken()
  }
}

The problem here still is, that when a user requests some data from the server and there is no token yet available (or it needs to be refreshed), the request is never sent.

We can solve this problem by saving the values needed for the URLRequest to an array and passing it back to the request(url:, completion:) function after we fetch or refresh the token. For this we need to create a wrapper structure that holds all these values.

struct OAuthRequest {
  var url: URL
  var completion: (Result<Any, Error>) -> Void
}

Since we are constructing the URLRequests concurrently we need to safely save them to the array to avoid any read/write conflicts.

private let oAuthRequestsQueue = DispatchQueue( label: "oAuthRequestsQueue", attributes: .concurrent)
private var unsafeOAuthRequests = Array<OAuthRequest>()

// safely adds the OAuthRequests to the array
private func safelyAddRequest(oAuthRequest: OAuthRequest) {
  oAuthRequestsQueue.async(flags: .barrier) { [weak self] in
    self?.unsafeOAuthRequests.append(oAuthRequest)
  }
}

So now, before fetching or refreshing a token, we save the OAuthRequest to the array by calling safelyAddRequest(oAuthRequest:). After the fetch or refresh is complete, we must also call the request(url:, completion:) function the same safe way and pass the url and completion from the previously saved OAuthRequest structure. That means the functions fetchToken() and refreshToken(token:) will now include the newly created safelySendAllRequests() function.

// fetches a new OAuth2Token
private func fetchToken() {
  // this is where you actually fetch the OAuth2Token from your API provider
  // after the fetch is complete, the new token must be assigned
  token = <#OAuth2Token#>
  // and then all the saved requests will be re-sent 
  safelySendAllRequests()
}
    
// refreshes the token using the current refresh token
private func refreshToken(tokenString: String) {
  // this is where you refresh the token with your API provider
  // after the refresh is complete, the refreshed token must be assigned
  token = <#OAuth2Token#>
  // and then all the saved requests will be re-sent 
  safelySendAllRequests()
}
    
// safely sends all saved OAuthRequests
private func safelySendAllRequests() {
  oAuthRequestsQueue.async(flags: .barrier) { [weak self] in
    self?.unsafeOAuthRequests.forEach { (oAuthRequest) in
      self?.request(url: oAuthRequest.url, completion: oAuthRequest.completion)
    }
    self?.unsafeOAuthRequests.removeAll()
  }
}

Now we have a valid token so the URLRequest will actually be passed onto the URLSession and the URLSessionDataTask will be executed.

Thats it. If you need to passt any parameters or additional headers to the request, don‘t forget to extend the OAuthRequest structure to also include them.

You can have a look at the complete code here.

Language: Swift 5.0 · Written on: iPad Pro