Moya: [Help] OAuth2 Authentication using Moya

Created on 1 Nov 2017  路  8Comments  路  Source: Moya/Moya

I am trying to do an OAuth2.0 Authentication using Moya and when I do the request I get 401 statusCode.

I created OAuthHandler:

import Foundation
import p2_OAuth2
import Alamofire


class OAuth2Handler {

    fileprivate let loader: OAuth2DataLoader

    init(oauth2: OAuth2) {
        loader = OAuth2DataLoader(oauth2: oauth2)
    }

}

/** 
 The RequestRetrier protocol allows a Request 
 that encountered an Error while being executed to be retried.
*/

extension OAuth2Handler: RequestRetrier {

    public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401, let req = request.request {
            var dataRequest = OAuth2DataRequest(request: req, callback: { _ in })
            dataRequest.context = completion
            loader.enqueue(request: dataRequest)
            loader.attemptToAuthorize() { authParams, error in
                self.loader.dequeueAndApply() { req in
                    if let comp = req.context as? RequestRetryCompletion {
                        comp(authParams != nil, 0.0)
                    }
                }
            }
        }
        else {
            completion(false, 0.0)  
        }
    }

}


/** 
 The RequestAdapter protocol allows each Request made on a SessionManager
 to be inspected and adapted before being created.
*/

extension OAuth2Handler: RequestAdapter {

    public func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        guard loader.oauth2.accessToken != nil else {
            return urlRequest
        }
        return try urlRequest.signed(with: loader.oauth2) 
    }

}

Then instantiating one SessionManager and setting its adapter and retrier

let sessionManager = SessionManager()
let oauthHandler = OAuth2Handler(oauth2: oauth2)
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler

Then I am passing the sessionManager to my provider

provider = MoyaProvider<Endpoint>(manager: sessionManager)

Lastly, when I do the request I see that it is unauthorised:

provider.request(.endpoint1). { request in
            switch request {
            case let .success(response):
                print(response.statusCode)
            case let .failure(error):
                print("error")
            }
        }

But when I am doing it like this, I can see that the request is authorised :

sessionManager.request("Endpoint1 URL").validate().responseJSON { response in
            if let res = response.value {
                print(res)
            }
        }

I expect to have an authorised response in the end when I do the request from my provider, but I don't understand why this is happening.

Can someone help me figuring out how to do the OAuth2 Authorisation in Moya?

question stale

Most helpful comment

// You can use this Endpoint closure to add token with header

private let endpointClosure = { (target: MyAPI) -> Moya.Endpoint<MyAPI> in
    let endpoint = MoyaProvider.defaultEndpointMapping(for: target);

    switch target {
    case .createuser,.refresh: //don't add token for this paths
        return endpoint
    default:
        //If there is a token add a token to header
        return endpoint.adding(newHTTPHeaderFields:["X-Api-Token": UserCredsManager.sharedInstance.getUser()?.token ?? ""])        
    }
}

If you want to retry your requests in case of failed request, try something like below

extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
    /// Tries to refresh auth token on 401 errors and retry the request.
    /// If the refresh fails, the signal errors.
    public func retryWithAuthIfNeeded() ->  Single<Response> {
        return self.retryWhen{ (e: Observable<Error>) in
            Observable.zip(e, Observable.range(start: 1, count: 3), resultSelector: { $1 }) //trying  3 times. You can change the number
                .flatMap { i in
                    return MyProvider.rx.request(.refresh(token: "abc")) //whatever your token endpoint for refreshing
                        .filterSuccessfulStatusAndRedirectCodes()
                        .map(Token.self)
                        .catchError {  error  in
                            if case MoyaError.statusCode(let response) = error  {
                                if response.statusCode == 401 {
                                    // Logout
                                    do {
                                        try User.logOut()
                                    } catch _ {
                                        logger.warning("Failed to logout")
                                    }
                                }
                            }
                            return Single.error(error)
                        }.flatMap { token -> Single<Token> in
                            do {
                                try token.saveInRealm()
                            } catch let e {
                                logger.warning("Failed to save access token")
                                return Single.error(e)
                            }
                            return Single.just(token)
                    }
            }
        }
    }
}

Use it like below

let disposeBag = DisposeBag()
let MyProvider = MoyaProvider<MyAPI>(endpointClosure:endpointClosure,plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter)])

MyProvider.rx.request(.allthings)
            .filterSuccessfulStatusCodes()
            .retryWithAuthIfNeeded()
        .map([Things].self)
        .subscribe(onSuccess: { response in
            print("Response")
            print(response)

        }) { error in
            print("Error occured")
            print(error)
        }
        .disposed(by: disposeBag)

All 8 comments

Have you ever used any of the Moya plugins before? That's how I do OAuth with Moya and it's much easier to use then what you have above.

There is a provided plugin Moya has for you to use just for this purpose

[Fixed] I forgot to add this to my Endpoit:

var validate: Bool { 
     return true
}

I am leaving this issue open for now if someone has other suggestions for this topic

Yeah, here is an example:

import Moya
import Result

public struct MoyaAppendHeadersPlugin: PluginType {

    public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var request = request
        if let urlString = request.url?.absoluteString, urlString.hasPrefix(AppConstants.apiEndpoint) {
            if let authToken = UserCredsManager.authToken {
                request.setValue(String(format: "Bearer %@", authToken), forHTTPHeaderField: "Authorization")
            }
        }
        return request
    }

    public func didReceive(_ result: Result<Moya.Response, Moya.MoyaError>, target: TargetType) {
    }

}

Then, when you construct your MoyaProvider, you pass in an instance of the plugin which is explained in this doc

My example is a little more involved because what I am using the UserCredsManager that I created myself which is saying, "whenever the app has set the auth token in the keychain, use it. If it doesn't exist, don't append it" which allows me to not have to care about state of my app.

// You can use this Endpoint closure to add token with header

private let endpointClosure = { (target: MyAPI) -> Moya.Endpoint<MyAPI> in
    let endpoint = MoyaProvider.defaultEndpointMapping(for: target);

    switch target {
    case .createuser,.refresh: //don't add token for this paths
        return endpoint
    default:
        //If there is a token add a token to header
        return endpoint.adding(newHTTPHeaderFields:["X-Api-Token": UserCredsManager.sharedInstance.getUser()?.token ?? ""])        
    }
}

If you want to retry your requests in case of failed request, try something like below

extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
    /// Tries to refresh auth token on 401 errors and retry the request.
    /// If the refresh fails, the signal errors.
    public func retryWithAuthIfNeeded() ->  Single<Response> {
        return self.retryWhen{ (e: Observable<Error>) in
            Observable.zip(e, Observable.range(start: 1, count: 3), resultSelector: { $1 }) //trying  3 times. You can change the number
                .flatMap { i in
                    return MyProvider.rx.request(.refresh(token: "abc")) //whatever your token endpoint for refreshing
                        .filterSuccessfulStatusAndRedirectCodes()
                        .map(Token.self)
                        .catchError {  error  in
                            if case MoyaError.statusCode(let response) = error  {
                                if response.statusCode == 401 {
                                    // Logout
                                    do {
                                        try User.logOut()
                                    } catch _ {
                                        logger.warning("Failed to logout")
                                    }
                                }
                            }
                            return Single.error(error)
                        }.flatMap { token -> Single<Token> in
                            do {
                                try token.saveInRealm()
                            } catch let e {
                                logger.warning("Failed to save access token")
                                return Single.error(e)
                            }
                            return Single.just(token)
                    }
            }
        }
    }
}

Use it like below

let disposeBag = DisposeBag()
let MyProvider = MoyaProvider<MyAPI>(endpointClosure:endpointClosure,plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter)])

MyProvider.rx.request(.allthings)
            .filterSuccessfulStatusCodes()
            .retryWithAuthIfNeeded()
        .map([Things].self)
        .subscribe(onSuccess: { response in
            print("Response")
            print(response)

        }) { error in
            print("Error occured")
            print(error)
        }
        .disposed(by: disposeBag)

Hey guys! I made a wrapper on TVDB API, and the API uses JWT token. This token expires after 24 hours. I managed to do the authentication and refresh the token automatically.
You can see more details here

This issue has been marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

This issue has been auto-closed because there hasn't been any activity for at least 21 days. However, we really appreciate your contribution, so thank you for that! 馃檹 Also, feel free to open a new issue if you still experience this problem 馃憤.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hamada147 picture hamada147  路  3Comments

geraldeersteling picture geraldeersteling  路  3Comments

JianweiWangs picture JianweiWangs  路  3Comments

JoeFerrucci picture JoeFerrucci  路  3Comments

kamwysoc picture kamwysoc  路  3Comments