8

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?

user6724161
  • 195
  • 2
  • 15

2 Answers2

4

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(...)
arturdev
  • 10,884
  • 2
  • 39
  • 67
  • 3
    From where `authenticationProvider.request` instance is coming from? – Alizain Prasla Oct 11 '19 at 19:27
  • 2
    What happens if multiple requests made? Does the refreshToken api called multiple times for each requests? – ugur Dec 18 '19 at 22:01
  • @ugur You should check if token is expired only then do a refresh, as written in the answer – arturdev Dec 19 '19 at 05:05
  • 6
    ok but imagine token is expired and simultaneous requests made at the same time. So in this case refresh token will be triggered for each requests and actual requests may resume with the wrong token assuming each refresh token expires the previous one. Alamofire RequestRetrier solves this with collecting requests in a list and resuming all of them after refresh token response. I m new to Moya am i missing something? – ugur Dec 20 '19 at 22:31
  • 1
    @how to use it for session expire? – Sourabh Sharma Jan 30 '20 at 06:00
1

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
    }
    
}

maginawin
  • 11
  • 4
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 21 '22 at 07:32