12

now I'm working on an iOS application in Swift 4. Here I'm using Alamofire to integrate the API calls. I need to integrate the right way to auto-refresh the authentication token and retry the previous API calls. I'm storing the authentication token once I logged in successfully. So after login, in each API, I'm appending the token in the header part. And when if the token is expired I will get 401. That time I need to auto-refresh the authentication token and recall the same API again. How can I do that? I checked in the Stackoverflow, but I didn't get any solution.

Here's my API Call,

import Foundation
import Alamofire
import SwiftyJSON

class LoveltyAPI {

    let loveltyURL = Bundle.main.object(forInfoDictionaryKey: "APIUrlString") as! String  // Main URL
    let buildVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String  //infoDictionary?["CFBundleShortVersionString"] as AnyObject
    weak var delegate:LoveltyProtocol?  

    func get_profile(app_user_id:String, token:String) {
        let urlString = "\(loveltyURL)\(get_profile_string)?app_user_id=\(app_user_id)"
        let headers = ["Content-Type":"application/json","X-Requested-With":"XMLHttpRequest", "Authentication":"Token \(token)"]
        Alamofire.request(urlString, method: .get, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
            switch response.result {
            case .success:
                let swiftyJsonVar = JSON(response.result.value!)
                switch response.response?.statusCode {
                case 200, 201:
                    self.delegate?.getUserProfile!(response: swiftyJsonVar["data"].dictionaryObject as AnyObject)
                case 401:
                    self.delegate?.tokenExpired(response: tokenExpired as AnyObject)
                case 404:
                    self.delegate?.serviceError!(response: swiftyJsonVar["message"] as AnyObject)
                case 422:
                    self.delegate?.serviceError!(response: swiftyJsonVar["error"] as AnyObject)
                case 503:
                    self.delegate?.appDisabled(response: swiftyJsonVar.dictionaryObject as AnyObject)
                default:
                    self.delegate?.serviceError!(response: self.serverError as AnyObject)
                }
            case .failure(let error):
                self.delegate?.serviceError!(response: self.serverError as AnyObject)
            }
        }
    }
}

Please help me. If you can explain with my code, it would be very nice.

Hilaj S L
  • 1,936
  • 2
  • 19
  • 31
  • Do you have separate API for refreshing the token or not? – Harjot Singh Jul 10 '19 at 05:14
  • Currently no, in the response of login API i'm storing the token in NSUserDefaults – Hilaj S L Jul 10 '19 at 05:21
  • Then you can take the user to the login page again to refresh the token and show the alert that "You have been Signed out" – Harjot Singh Jul 10 '19 at 05:22
  • Currently, I'm doing the same, if 401 means the app will forcefully logout. But the client doesn't want to logout the users. – Hilaj S L Jul 10 '19 at 05:24
  • Then, you can save the login credentials and relogin the user in the background OR alternatively, make the refresh token API. – Harjot Singh Jul 10 '19 at 05:27
  • So we're planned to make a new API call for refresh Token, and once if we got 401, we need to call that, and that success response I need to recall the previous API with the new token. – Hilaj S L Jul 10 '19 at 05:28
  • But I don't know how to call the previous API automatically? – Hilaj S L Jul 10 '19 at 05:28
  • using the delegate when you have successfully refreshed the token, you can call the previous API. – Harjot Singh Jul 10 '19 at 05:31
  • But In one VC, I have 3-4 API calls, and how can I find which API I need to recall? – Hilaj S L Jul 10 '19 at 05:32
  • make the bools for separate APIs and change their value when you got success. – Harjot Singh Jul 10 '19 at 05:35
  • For example, In my profile view controller class, I have two API's, getProfileDetails and updateProfileDetails. Maybe I got 401 in the getProfileDetails API or updateProfileDetails API. And when the token expired I got the callback in the delegate method in the same VC, but how can I identify which API I got 401. – Hilaj S L Jul 10 '19 at 05:36

3 Answers3

24

You need Alamofire RequestRetrier and RequestAdapter check here

This is some example that I have:

import UIKit
import Alamofire

class MyRequestAdapter: RequestAdapter, RequestRetrier {
    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void

    private let lock = NSLock()

    private var isRefreshing = false
    private var requestsToRetry: [RequestRetryCompletion] = []
    var accessToken:String? = nil
    var refreshToken:String? = nil
    static let shared = MyRequestAdapter()

    private init(){
        let sessionManager = Alamofire.SessionManager.default
        sessionManager.adapter = self
        sessionManager.retrier = self
    }

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(BASE_URL), !urlString.hasSuffix("/renew") {
            if let token = accessToken {
                urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            }
        }
        return urlRequest
    }


    // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken {
                        strongSelf.accessToken = accessToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

    // MARK: - Private - Refresh Tokens

    private func refreshTokens(completion: @escaping RefreshCompletion) {
        guard !isRefreshing else { return }

        isRefreshing = true

        let urlString = "\(BASE_URL)token/renew"

        Alamofire.request(urlString, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: ["Authorization":"Bearer \(refreshToken!)"]).responseJSON { [weak self] response in
            guard let strongSelf = self else { return }
            if
                let json = response.result.value as? [String: Any],
                let accessToken = json["accessToken"] as? String
            {
                completion(true, accessToken)
            } else {
                completion(false, nil)
            }
            strongSelf.isRefreshing = false
        }

    }
}

My example is a little bit complex, but yes in general we have two important methods first one is adapt(_ urlRequest: URLRequest) throws -> URLRequest where we attaching the token, here I have custom logic where one of the services have should not attach this token as a header. The second method is func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) where I check what is the error code(in my example 401). And then I refresh my tokens with

 private func refreshTokens(completion: @escaping RefreshCompletion)

In my case, I have refresh token and access token and when I call the service with refresh token I should not append my old access token in the header. I think this is not best practice but it was implemented from peopele that I don't know.

m1sh0
  • 2,236
  • 1
  • 16
  • 21
  • I still have doubts with alamofire. I understand the above methods, but how to make the alamofire request? – Hilaj S L Jul 10 '19 at 12:44
  • In API class, I'm making the request like this Alamofire.request(urlString, method: .get, encoding: JSONEncoding.default, headers: headers).responseJSON { response in – Hilaj S L Jul 10 '19 at 12:45
  • In refreshToken method I just calling the request for refresh and after the response, I update my token and using the callback function to do the retry the previous call. Also, you need to call at least once MyRequestAdapter.shared in order to set the default request adapter and retrier(check what I do in init). – m1sh0 Jul 10 '19 at 12:59
8

You can easily Refresh token and retry your previous API call using

Alamofire RequestInterceptor

NetworkManager.swift

import Alamofire
    class NetworkManager {
        static let shared: NetworkManager = {
            return NetworkManager()
        }()
        typealias completionHandler = ((Result<Data, CustomError>) -> Void)
        var request: Alamofire.Request?
        let retryLimit = 3
        
        func request(_ url: String, method: HTTPMethod = .get, parameters: Parameters? = nil,
                     encoding: ParameterEncoding = URLEncoding.queryString, headers: HTTPHeaders? = nil,
                     interceptor: RequestInterceptor? = nil, completion: @escaping completionHandler) {
            AF.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers, interceptor: interceptor ?? self).validate().responseJSON { (response) in
                if let data = response.data {
                    completion(.success(data))
                } else {
                    completion(.failure())
                }
            }
        }
        
    }

RequestInterceptor.swift

  import Alamofire
extension NetworkManager: RequestInterceptor {
    
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var request = urlRequest
        guard let token = UserDefaultsManager.shared.getToken() else {
            completion(.success(urlRequest))
            return
        }
        let bearerToken = "Bearer \(token)"
        request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
        print("\nadapted; token added to the header field is: \(bearerToken)\n")
        completion(.success(request))
    }
    
    func retry(_ request: Request, for session: Session, dueTo error: Error,
               completion: @escaping (RetryResult) -> Void) {
       guard let statusCode = request.response?.statusCode else {
        completion(.doNotRetry)
        return
    }
    
    guard request.retryCount < retryLimit else {
        completion(.doNotRetry)
        return
    }
    print("retry statusCode....\(statusCode)")
    switch statusCode {
    case 200...299:
        completion(.doNotRetry)
    case 401:
        refreshToken { isSuccess in isSuccess ? completion(.retry) : completion(.doNotRetry) }
        break
    default:
        completion(.retry)
    } 
    }
    
    func refreshToken(completion: @escaping (_ isSuccess: Bool) -> Void) {
                let params = [
"refresh_token": Helpers.getStringValueForKey(Constants.REFRESH_TOKEN)
        ]
        AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default).responseJSON { response in
            if let data = response.data, let token = (try? JSONSerialization.jsonObject(with: data, options: [])
                as? [String: Any])?["access_token"] as? String {
                UserDefaultsManager.shared.setToken(token: token)
                print("\nRefresh token completed successfully. New token is: \(token)\n")
                completion(true)
            } else {
                completion(false)
            }
        }
    }
    
}

Alamofire v5 has a property named RequestInterceptor. RequestInterceptor has two method, one is Adapt which assign access_token to any Network call header, second one is Retry method. In Retry method we can check response status code and call refresh_token block to get new token and retry previous API again.

M Mahmud Hasan
  • 1,303
  • 1
  • 13
  • 20
  • 1
    This is a partial solution that only solves simple cases. Have you thought what happens if there are more than 1 requests triggered at once and all of them request token refresh at the same time? – Borut Tomazin Jul 06 '22 at 07:08
  • if the token has been refreshed once why you need to refresh again for other request? – M Mahmud Hasan Jul 19 '22 at 10:37
3

@m1sh0's answer was extremely helpful to me. I'm just adding the missing detail the OP asked for in the comments: How do you make the Alamofire request so that it uses the Retrier and Adapter?

I basically used @m1sh0's example and called it like this:

        var request_url = Constants.API_URL + "/path/to/resource"

        let sessionManager = Alamofire.SessionManager.default
        sessionManager.adapter = MyRequestAdapter.shared
        
        sessionManager.request(request_url).validate().responseJSON { (response: DataResponse<Any>) in
            switch(response.result) {
            case .success(_):
                print(response.result.value!)
                completion(response.result.value!)
            case .failure(_):
                print(response.result.error!)
                completion(response.result.error!)
                break
            }
        }

Note that you need validate() in the request in order to get retried on failure. Without it, the response is just returned for completion. Also note there's a failure case in the response block for all non-401 errors, as they are presumed unrecoverable.

seanthomas
  • 41
  • 2