'Refreshing auth token with Moya

I'm using Moya to communicate with my API. For many of my endpoints, I require that the user be authenticated (i.e. a bearer token is based in the Authorization header).

In the Moya documentation, here, I found how to include the Authorization header, along with the bearer token.

However, I now need to implement auth token refreshing, and I'm not sure how to do this.

I found this thread on Moya's Github with an answer that looks like it might help, but I have no idea where to put the code. Here is what the answer's code looks like:

// (Endpoint<Target>, NSURLRequest -> Void) -> Void
static func endpointResolver<T>() -> MoyaProvider<T>.RequestClosure where T: TargetType {
    return { (endpoint, closure) in
        let request = endpoint.urlRequest!
        request.httpShouldHandleCookies = false

        if (tokenIsOK) {
            // Token is valid, so just resume the request and let AccessTokenPlugin set the Authentication header
            closure(.success(request))
            return
        }
        // authenticationProvider is a MoyaProvider<Authentication> for example
        authenticationProvider.request(.refreshToken(params)) { result in
            switch result {
                case .success(let response):
                    self.token = response.mapJSON()["token"]
                    closure(.success(request)) // This line will "resume" the actual request, and then you can use AccessTokenPlugin to set the Authentication header
                case .failure(let error):
                    closure(.failure(error)) //something went terrible wrong! Request will not be performed
            }
        }
    }
}

And here is my class for my Moya provider:

import Foundation
import Moya

enum ApiService {
    case signIn(email: String, password: String)
    case like(id: Int, type: String)
}

extension ApiService: TargetType, AccessTokenAuthorizable {
    var authorizationType: AuthorizationType {
        switch self {
        case .signIn(_, _):
            return .basic
        case .like(_, _):
            return .bearer
        }
    }

    var baseURL: URL {
        return URL(string: Constants.apiUrl)!
    }

    var path: String {
        switch self {
            case .signIn(_, _):
                return "user/signin"
            case .like(_, _):
                return "message/like"
        }
    }

    var method: Moya.Method {
        switch self {
            case .signIn, .like:
                return .post
        }
    }

    var task: Task {
        switch self {
            case let .signIn(email, password):
                return .requestParameters(parameters: ["email": email, "password": password], encoding: JSONEncoding.default)
            case let .like(id, type):
                return .requestParameters(parameters: ["messageId": id, "type": type], encoding: JSONEncoding.default)
        }
    }

    var sampleData: Data {
        return Data()
    }

    var headers: [String: String]? {
        return ["Content-type": "application/json"]
    }
}

private extension String {
    var urlEscaped: String {
        return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }

    var utf8Encoded: Data {
        return data(using: .utf8)!
    }
}

Where would I put the answer's code in my code? Am I missing something?



Solution 1:[1]

Actually, that example is a bit old. So here is a new one:

extension MoyaProvider {
    convenience init(handleRefreshToken: Bool) {
        if handleRefreshToken {
            self.init(requestClosure: MoyaProvider.endpointResolver())
        } else {
            self.init()
        }
    }

    static func endpointResolver() -> MoyaProvider<Target>.RequestClosure {
        return { (endpoint, closure) in
            //Getting the original request
            let request = try! endpoint.urlRequest()

            //assume you have saved the existing token somewhere                
            if (#tokenIsNotExpired#) {                   
                // Token is valid, so just resume the original request
                closure(.success(request))
                return
            }

            //Do a request to refresh the authtoken based on refreshToken
            authenticationProvider.request(.refreshToken(params)) { result in
                switch result {
                case .success(let response):
                    let token = response.mapJSON()["token"]
                    let newRefreshToken = response.mapJSON()["refreshToken"]
                    //overwrite your old token with the new token
                    //overwrite your old refreshToken with the new refresh token

                    closure(.success(request)) // This line will "resume" the actual request, and then you can use AccessTokenPlugin to set the Authentication header
                case .failure(let error):
                    closure(.failure(error)) //something went terrible wrong! Request will not be performed
                }
            }
    }
}

Usage:

public var provider: MoyaProvider<SomeTargetType> = MoyaProvider(handleRefreshToken: true)

provider.request(...)

Solution 2:[2]

I use a plugin to refresh token.

class ApiManager {
    
    static let shared = ApiManager()
    
    private(set) var srAccountProvider: MoyaProvider<SRAccountApi>!
    
    private init() {
        
        let refreshTokenPlugin = RefreshTokenPlugin()
        srAccountProvider = MoyaProvider<SRAccountApi>(plugins: [refreshTokenPlugin])
    }
    
}
//
//  RefreshTokenPlugin.swift
//  Moya Token
//
//  Created by maginawin on 2022/4/20.
//

import Foundation
import Moya

public class RefreshTokenPlugin: PluginType {
    
    private var semaphore = DispatchSemaphore(value: 0)
    
    public init() {
        
    }
    
    public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        
        NSLog("prepare request", "")
        
        guard let authorizable = target as? AccessTokenAuthorizable,
              let authorizationType = authorizable.authorizationType else {
            
            return request
        }
        
        let now = Date().timeIntervalSince1970
        
        // if less than 1 hour, refresh token.
        if (TokenManager.shared.expiredTimestamp - now) < 3600, let refreshToke = TokenManager.shared.refreshToken  {
            
            NSLog("start refresh token automatic", "")
            
            let provider = MoyaProvider<SRAccountApi>()
            
            // refresh token once
            provider.request(.refreshToken(refreshToken: refreshToke), callbackQueue: DispatchQueue.global()) { result in
                
                defer {
                    self.semaphore.signal()
                }
                
                do {

                    let response = try result.get()
                    let value = try response.mapJSON() as? [String: Any]

                    if let code = value?["code"] as? String,
                       let message = value?["message"] as? String,
                       code == "10001",
                       let data = value?["data"] as? [String: Any],
                       let authorization = data["authorization"] as? [String: Any] {

                        print("refresh token successful! \(code) \(message) \(data)")
                        
                        // Update tokens and expired timestamp.
                        TokenManager.shared.accessToken = authorization["accessToken"] as? String ?? ""
                        TokenManager.shared.refreshToken = authorization["refreshToken"] as? String
                        TokenManager.shared.expiredTimestamp = (authorization["expiredTimestamp"] as? TimeInterval ?? 0) / 1000
                        
                    } else {
                        
                        print("refresh token failed!")
                        TokenManager.shared.refreshToken = nil
                    }

                } catch {

                    NSLog("error %@", error.localizedDescription)
                    TokenManager.shared.refreshToken = nil
                }
            }
            
            semaphore.wait()
        }
        
        var request = request
        let authValue = authorizationType.value + " " + TokenManager.shared.accessToken
        request.addValue(authValue, forHTTPHeaderField: "Authorization")

        return request
    }
    
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 maginawin