10

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?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
CoderMan
  • 178
  • 3
  • 12

1 Answers1

0

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
    }
}
ephemer
  • 1,239
  • 8
  • 21