aaisataev
aaisataev

Reputation: 1683

Right way to refresh the token

There is a function getUser in RequestManager class that called in my VC.

func getUser(onCompletion: @escaping (_ result: User?, error: String?) -> Void) {
    Alamofire.request(Router.getUser).responseJSON { (response) in
        // here is the work with response
    }
}

If this request returns 403 it means access_token is expired. I need to refresh token and repeat the request from my VC.

Now the question.

How to refresh token and repeat the request in the right way?

To handle the error and refresh token in MyViewController or getUser method is not good idea because I have a lot of VCs and request methods.

I need something like: VC calls the method and gets the User even if token is expired and refreshToken must not be in all request methods.

EDIT

refreshToken method

func refreshToken(onCompletion: @escaping (_ result: Bool?) -> Void) {
    Alamofire.request(Router.refreshToken).responseJSON { (response) in
        print(response)
        if response.response?.statusCode == 200 {
            guard let data = response.data else { return onCompletion(false) }
            let token = try? JSONDecoder().decode(Token.self, from: data)
            token?.setToken()
            onCompletion(true)
        } else {
            onCompletion(false)
        }
    }
}

Upvotes: 14

Views: 19439

Answers (3)

M Mahmud Hasan
M Mahmud Hasan

Reputation: 1173

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.

Upvotes: 1

vboyko
vboyko

Reputation: 179

You can create generic refresher class:

protocol IRefresher {
    associatedtype RefreshTarget: IRefreshing

    var target: RefreshTarget? { get }

    func launch(repeats: Bool, timeInterval: TimeInterval)
    func invalidate()
}

class Refresher<T: IRefreshing>: IRefresher {

    internal weak var target: T?
    private var timer: Timer?

    init(target: T?) {
        self.target = target
    }

    public func launch(repeats: Bool, timeInterval: TimeInterval) {
        timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { [weak self] (timer) in
            self?.target?.refresh()
        }
    }

    public func invalidate() {
        timer?.invalidate()
    }
}

And the refresh target protocol:

protocol IRefreshing: class {
    func refresh()
}

Define new typealias:

typealias RequestManagerRefresher = Refresher<RequestManager>

Now create refresher and store it:

class RequestManager {
    let refresher: RequestManagerRefresher

    init() {
        refresher = Refresher(target: self)
        refresher?.launch(repeats: true, timeInterval: 15*60)
    }
}

And expand RequestManager:

extension RequestManager: IRefreshing {
    func refresh() {
        updateToken()
    }
}

Every 15 minutes your RequestManager's token will be updated


UPDATE

Of course, you also can change the update time. Create a static var that storing update time you need. For example inside the RequestManager:

class RequestManager {
    static var updateInterval: TimeInterval = 0

    let refresher: RequestManagerRefresher

    init() {
        refresher = Refresher(target: self)
        refresher?.launch(repeats: true, timeInterval: updateInterval)
    }
}

So now you can ask the token provider server for token update interval and set this value to updateInterval static var:

backendTokenUpdateIntervalRequest() { interval in
    RequestManager.updateInterval = interval
}

Upvotes: 1

Ankit Jayaswal
Ankit Jayaswal

Reputation: 5679

To solve this, I created a class from which we will call every API, say BaseService.swift.

BaseService.swift :

import Foundation
import Alamofire
import iComponents

struct AlamofireRequestModal {
    var method: Alamofire.HTTPMethod
    var path: String
    var parameters: [String: AnyObject]?
    var encoding: ParameterEncoding
    var headers: [String: String]?

    init() {
        method = .get
        path = ""
        parameters = nil
        encoding = JSONEncoding() as ParameterEncoding
        headers = ["Content-Type": "application/json",
                   "X-Requested-With": "XMLHttpRequest",
                   "Cache-Control": "no-cache"]
    }
}

class BaseService: NSObject {

    func callWebServiceAlamofire(_ alamoReq: AlamofireRequestModal, success: @escaping ((_ responseObject: AnyObject?) -> Void), failure: @escaping ((_ error: NSError?) -> Void)) {

        // Create alamofire request
        // "alamoReq" is overridden in services, which will create a request here
        let req = Alamofire.request(alamoReq.path, method: alamoReq.method, parameters: alamoReq.parameters, encoding: alamoReq.encoding, headers: alamoReq.headers)

        // Call response handler method of alamofire
        req.validate(statusCode: 200..<600).responseJSON(completionHandler: { response in
            let statusCode = response.response?.statusCode

            switch response.result {
            case .success(let data):

                if statusCode == 200 {
                    Logs.DLog(object: "\n Success: \(response)")
                    success(data as AnyObject?)

                } else if statusCode == 403 {
                    // Access token expire
                    self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)

                } else {
                    let errorDict: [String: Any] = ((data as? NSDictionary)! as? [String: Any])!
                    Logs.DLog(object: "\n \(errorDict)")
                    failure(errorTemp as NSError?)
                }
            case .failure(let error):
                Logs.DLog(object: "\n Failure: \(error.localizedDescription)")
                failure(error as NSError?)
            }
        })
    }

}

extension BaseService {

    func getAccessToken() -> String {
        if let accessToken =  UserDefaults.standard.value(forKey: UserDefault.userAccessToken) as? String {
            return "Bearer " + accessToken
        } else {
            return ""
        }
    }

    // MARK: - API CALL
    func requestForGetNewAccessToken(alaomReq: AlamofireRequestModal, success: @escaping ((_ responseObject: AnyObject?) -> Void), failure: @escaping ((_ error: NSError?) -> Void) ) {

        UserModal().getAccessToken(success: { (responseObj) in
            if let accessToken = responseObj?.value(forKey: "accessToken") {
                UserDefaults.standard.set(accessToken, forKey: UserDefault.userAccessToken)
            }

            // override existing alaomReq (updating token in header)
            var request: AlamofireRequestModal = alaomReq
            request.headers = ["Content-Type": "application/json",
                               "X-Requested-With": "XMLHttpRequest",
                               "Cache-Control": "no-cache",
                               "X-Authorization": self.getAccessToken()]

            self.callWebServiceAlamofire(request, success: success, failure: failure)

        }, failure: { (_) in
            self.requestForGetNewAccessToken(alaomReq: alaomReq, success: success, failure: failure)
        })
    }

}

For calling the API from this call, we need to create a object of AlamofireRequestModal and override it with necessary parameter.

For example I created a file APIService.swift in which we have a method for getUserProfileData.

APIService.swift :

import Foundation

let GET_USER_PROFILE_METHOD = "user/profile"

struct BaseURL {
    // Local Server
    static let urlString: String = "http://192.168.10.236: 8084/"
    // QAT Server
    // static let urlString: String = "http://192.171.286.74: 8080/"

    static let staging: String = BaseURL.urlString + "api/v1/"
}

class APIService: BaseService {

    func getUserProfile(success: @escaping ((_ responseObject: AnyObject?) -> Void), failure: @escaping ((_ error: NSError?) -> Void)) {

        var request: AlamofireRequestModal = AlamofireRequestModal()
        request.method = .get
        request.path = BaseURL.staging + GET_USER_PROFILE_METHOD
        request.headers = ["Content-Type": "application/json",
                           "X-Requested-With": "XMLHttpRequest",
                           "Cache-Control": "no-cache",
                           "X-Authorization": getAccessToken()]

        self.callWebServiceAlamofire(request, success: success, failure: failure)
    }

}

Explanation:

In code block:

else if statusCode == 403 {
    // Access token expire
    self.requestForGetNewAccessToken(alaomReq: alamoReq, success: success, failure: failure)
}

I call getNewAccessToken API (say refresh-token, in your case), with the request( it could be any request based from APIService.swift).

When we get new token I save it user-defaults then I will update the request( the one I am getting as a parameter in refresh-token API call), and will pass the success and failure block as it is.

Upvotes: 24

Related Questions