7

I've searched a lot before I post my question but unfortunately I wasn't able to find a solution to my question.

I develop an app that connects to a server that requires authentication using access token and refresh token.

  • The access token is valid for 1 hour and can be used many times.

  • The refresh token is valid for 1 month and is used when the access token expires. The refresh token can be used only one time.

When the refresh token is used, I get a new refresh token as well in addition to the access token.

And here is my problem:

I wrote an APIClient class that handles all request that my app needs. This class works great, except when the access token expires. When the access token expires all requests that will run at this time will fail with a 401 code (unauthorized).

What I want is to find a solution that will refresh the access token using the refresh token and then retry all these requests that failed with status code 401. Keep in mind that the function that refreshes the token must be called only once because the refresh token is only valid for one use.

What I would like to do is to find a way to write my APIClient class so that it supports the refresh of the token and retry all requests that failed. I would be very grateful if you tell me how i can achieve this.

Take a look at the following source code of the getRequest and sendRefreshRequest.

func getRequestWith(requestType: FSRequestType, usingToken: Bool, parameters: RequestParameters?, completionClosure: @escaping (NetworkResult) -> Void) {
    let sessioConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessioConfig, delegate: nil, delegateQueue: nil)
    guard var URL = APIClient.baseURL?.appendingPathComponent(requestType.rawValue) else { return }

    if let parameters = parameters {
        URL = URL.appendingQueryParameters(parameters)
    }

    var request = URLRequest(url: URL)
    request.httpMethod = "GET"

    if usingToken {
        let tokenString = "Bearer " + TokenManager.sharedManager.accessToken()!

        request.addValue(tokenString, forHTTPHeaderField: "Authorization")
    }

    let task = session.dataTask(with: request) { (data, response, error) in
        guard error == nil else { return completionClosure(.Error(error!.localizedDescription))}
        guard let data = data else { return completionClosure(.Error("Could not load data")) }

        let statusCode = (response as! HTTPURLResponse).statusCode

        if statusCode == 401 {
            //Handle refresh token
        } else if statusCode == 200 {
            let json = JSON(data: data)
            let responseJSON = json["response"]
            completionClosure(.Success(responseJSON))
        } else {
            completionClosure(.Error("Returned status code \(statusCode) from Get request with URL: \(request.url!)"))
        }
    }
    task.resume()
}

func sendRefreshRequest() {
    let session = URLSession(configuration: .default)

    guard var URL = APIClient.baseURL?.appendingPathComponent(FSRequestType.RefreshToken.rawValue) else {return}

    let URLParams = [
        "refresh_token" : TokenManager.sharedManager.refreshToken()!
    ]

    URL = URL.appendingQueryParameters(URLParams)
    var request = URLRequest(url: URL)
    request.httpMethod = "GET"

    let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
        let statusCode = (response as! HTTPURLResponse).statusCode

        if statusCode == 200 {

            let json = JSON(data: data!)
            if TokenManager.sharedManager.saveTokenWith(json) {
                print("RefreshedToken")
            } else {
                print("Failed saving new token.")
                SessionManager.sharedManager.deauthorize()
            }
        } else if statusCode == 400 {
            print("The refresh token is invalid")
            SessionManager.sharedManager.deauthorize()
        }
    })
    task.resume()
}

Thanks

rmaddy
  • 314,917
  • 42
  • 532
  • 579
zarzonis
  • 71
  • 4
  • 1
    Hi Sneak. Thank you for editing my question. It is now more readable. I edited my question and i added the getRequest and refreshToken functions. I hope that my question is not too broad now. – zarzonis Mar 26 '17 at 10:47
  • Great edit. I have retracted the too broad flag and hope you get some help now. I will look into it myself later when I get home if you don't get any help by then. –  Mar 26 '17 at 11:02

2 Answers2

1

I don't code in Swift so I can't provide you with code, however I think the easiest way is to:

1: Define your completionBlock inside APIClient

2: Handle all the calls internally inside your APIClient, in your if (statusCode == 401) handler,

3: Once you have finished refreshing the tokens, you can check if the completionBlock != nil, request the data again, and return the callback on success.

While you are doing all the refresh methods inside the APIClient, you don't return the completionBlock until you have finished refreshing and retried the request again .

Your other class that is calling the APIClient will wait for the completionBlock to be returned, and you can pass around the completionBlock until you get the correct tokens back and have made your retried calls.

Check these threads for the code on how to define the completionBlock Swift, you get the idea.

Another Example 1

Another example 2

Another Example 3

Community
  • 1
  • 1
0

Assuming I understand the problem correctly, what you describe lends itself to the singleton pattern. Have a singleton that stores the current tokens and serves as a request gate.

Pass in a completion block that contains the code to start a new request and takes a parameter for the access token. If a token is available, it calls the block immediately with the token. Otherwise, for the first block, it calls it with the refresh token instead and nils out the refresh token. When you get a response, call a method to store the new access token, at which point it calls the other blocks with the access token. If a request fails, tell the singleton that token xyzz135 or whatever is invalid, and if that is still the active token, it should nil it out. Then redo the attempt to run the request block with a token. If a new token has already been obtained by another request, it will run immediately, else if there's already a request trying, it will wait, else it will be restarted with the refresh token.

Alternatively, you can do the response handling inside that singleton and make the outside code not care about authentication at all. Either way, the logic is still the same.

dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • Hi, dgatwood. The APIClient and TokenManager have already a singleton. The TokenManager saves and returns the tokens from Keychain. The classes that use the APIClient should not be responsible for calling the networking code because then I'll end up having the same code to too many places. The response handling should be done inside the APIClient. Also, when a request fails, I make a new request that refreshes both tokens. When that returns, I need to retry every request that failed. Do you mind explaining your answer a little more and if possible add some sample code? Thanks for your time. – zarzonis Mar 27 '17 at 11:15
  • Essentially, everything from "if usingToken" to the end of the function becomes a block that takes two parameters: the refresh token and the access token. This gets assigned to a variable. You then add code to handle adding the refresh token to the request if not nil. You pass that block as a parameter to a method on the TokenManager class. It checks if the token is nil, and if not, it calls the block and passes it the token. – dgatwood Mar 28 '17 at 03:18
  • If it is nil, it uses a BOOL to decide what to do. If NO (the default state), it passes the refresh token to the block and sets the BOOL to YES. If the BOOL is YES, it adds the block to an array. Then in your APIClient block, you add two extra bits of code: 1. If you were passed a refresh token, call a method on the TokenManager class that sets the access token and then iterates through the array, calling each of the blocks with that token. 2. If a request failed with a 401 (or whatever), call a token invalidation method on TokenManager. – dgatwood Mar 28 '17 at 03:21
  • That method takes the same block, and checks if the token is current. If so, it sets the stored access token to nil. Either way, it then passes the block to the original request method. And you're done except for maybe some error handling cases. – dgatwood Mar 28 '17 at 03:23