Moya: Use AccessTokenPlugin only after login

Created on 25 Oct 2017  路  15Comments  路  Source: Moya/Moya

Hi,
I am trying to use the AccessTokenPlugin functionality and i have some issue with it. I need to find a way to initiate the AccessTokenPlugin only after my users are logged in and they have a token.
Currently in my Moya implementation I am creating the provider in the following way:

let token = ""  
let authPlugin = AccessTokenPlugin(tokenClosure: token)
let myProvider = MoyaProvider<API>(plugins: [authPlugin])


enum API {
    case loginUser(username: String, password: String)
    case createUser(user: User)
}

extension API : TargetType, AccessTokenAuthorizable {
...

I need the JWT bearer to all the requests except the login and the creation of new users.
The problem is that i need to find a way to set the token only after the users are logged in. I was thinking about creating a new provider and override the old one but i don't understand if this is the recommended way to go with.

Can you please advice?

Thanks.

question

Most helpful comment

Oh, I think I understand the issue now. We had this problem before, so we changed the plugin lately that you don't have to specify token when registering the provider. Now we have a tokenClosure, instead of a token, so you can specify a way of getting your token instead of a token itself. We also added a convenience autoclosure keyword, so even a string is accepted by this plugin (thus why the code you posted works as well).

So, let's say that you save your token in a keychain. You can initialize a provider even though you don't have your token yet:

let tokenClosure: () -> String = {
    return Keychain.getToken() // here you can specify how to retrieve a token
}
let authPlugin = AccessTokenPlugin(tokenClosure: tokenClosure)
let myProvider = MoyaProvider<API>(plugins: [authPlugin])

Does this resolve your problem now, @ranhsd?

All 15 comments

Hey @ranhsd. Right now to add a token you need to specify when/how you want to add it (by implementing AccessTokenAuthorizable) and what you want to add (by using tokenClosure). The first one should give you the ability to manipulate when/how you want to add it. You can have a default extension that you would always want to add the token, but in your login/register endpoint you would not send it with the request, e.g.:

extension API: AccessTokenAuthorizable {
    var authorizationType: AuthorizationType {
        switch self {
            case .loginUser, .createUser: .none
            default: return .bearer
        }
    }
}

Let me know if it's what you are looking for.

Hi @sunshinejr
Thanks for your quick response.
It鈥檚 really what I meant. I need to know when I can set the plugin to the provider. Because the first time that I create the provider and the user is not logged in I don鈥檛 really have the token I need to set the plugin on the provider only when I really have it (after user logged in and access token returns from the server) but I already initiate the provider before with the plugin. I need to find a way to recreate the provider again but this time to send the access token to the plugin

If you need more info please let me know

Thanks,
Ran

Oh, I think I understand the issue now. We had this problem before, so we changed the plugin lately that you don't have to specify token when registering the provider. Now we have a tokenClosure, instead of a token, so you can specify a way of getting your token instead of a token itself. We also added a convenience autoclosure keyword, so even a string is accepted by this plugin (thus why the code you posted works as well).

So, let's say that you save your token in a keychain. You can initialize a provider even though you don't have your token yet:

let tokenClosure: () -> String = {
    return Keychain.getToken() // here you can specify how to retrieve a token
}
let authPlugin = AccessTokenPlugin(tokenClosure: tokenClosure)
let myProvider = MoyaProvider<API>(plugins: [authPlugin])

Does this resolve your problem now, @ranhsd?

Hi @sunshinejr
it looks better. The question is if the tokenClosure will be executed in each api call ?

Thanks!
Ran.

Yes, @ranhsd. Each time there is a request, the plugin evaluates the tokenClosure just before, as you can see here. No caching involved.

@sunshinejr Thank you very much!!

Glad it helped! 馃帀

@sunshinejr authPlugin doesn't seem to work when using <MultiTarget> like below. Is there a way to implement that? Seems strange because NetworkLoggerPlugin works fine using <MultiTarget>

let tokenClosure: () -> String = {
    return Keychain.getToken() // here you can specify how to retrieve a token
}
let authPlugin = AccessTokenPlugin(tokenClosure: tokenClosure)
let provider = MoyaProvider<MultiTarget>(plugins: [authPlugin])

@sunshinejr Why tokenClosure, not tokenClosure: (TargetType) -> String
I have two 2+ accessTokens for different endPoints and 3 types:
basic, bearer and custom - "token"

I think AccessTokenPlugin good only for static accessToken.

public enum CustomAthorizationType {

    case none
    case basic// = "Basic"
    case bearer// = "Bearer"
    case custom(String)

    var value: String {
        switch self {
        case .none: return ""
        case .basic, .bearer: return String(describing: self).capitalized
        case .custom(let custom): return custom
        }
    }
}
    static var plugins: [PluginType] {
        return [
            CustomAccessTokenPlugin(tokenClosure: { target -> String in
                switch target {
                case Target.request(let data):
                    guard
                        let bytes = try? HMAC(key: encryptingKey, variant: .sha1).authenticate(data.bytes),
                        let encrypted = bytes.toBase64() else { return "" }

                    return encrypted

                case Target.login:
                    guard let loginData = String(format: "%@:%@", appClientUsername, appClientPassword).data(using: .utf8)
                        else { return "" }

                    return loginData.base64EncodedString()
                default: return ""
                }
            }),
            NetworkLoggerPlugin(verbose: true, cURL: true)]
    }

Is the encapsulation broken in this way?
I can try to extend the plugin and make a pull request.

@SeRG1k17 I think you can use two different providers, each with a different plugin for your request. We have an open PR for updating the AccessTokenPlugin. Maybe you would like to review the PR and provide any input there

Sent with GitHawk

@kavanbrandon Hey, my guess would be that MultiTarget doesn't conform to AccessTokenAuthorizable and you would have to do it yourself:

extension MultiTarget: AccessTokenAuthorizable {
    var authorizationType: AuthorizationType {
        // here you will have to check whether the `target` is also conforming to `AccessTokenAuthorizable` or not...
    }
}

@SeRG1k17 Hmm, using TargetType as parameter to tokenClosure seems fine with me, as @SD10 it would be awesome if you would provide your feedback in the PR that improves this plugin (#1521). Current workaround would be to use multiple providers for multiple access tokens.

@sunshinejr Thanks!

@sunshinejr @SD10 I do not really want to create two separate providers. Without this, there will be a violation of code cleanliness?
Also i have two different baseURL, and it`s my solution:

//EndPoints.swift

    enum BaseURL {
        case azure(EndPoints)
        case cloudapp(EndPoints)

        static func type(for endpoint: EndPoints) -> BaseURL {
            switch endpoint {
            case .request: return .cloudapp(endpoint)
            case .login: return .azure(endpoint)
            }
        }

        var url: URL {

            var url: String!
            switch self {
            case .azure: url = "https://"
            case .cloudapp: url = "http://t"
            }

            return URL(string: url)!
        }
    }

    public var urlType: BaseURL { return BaseURL.type(for: self) }
    public var baseURL: URL { return urlType.url }
//...

It`s possible create this as generic.

I think we can make this plugin more convenient, so that the headers are not static.

@sunshinejr

Hi, it is having an error when I follow your sample code.

_Xcode Error: "Function produces expected type 'String'; did you mean to call it with '()'?"_

Could you advise this issue, please?

let tokenClosure: () -> String = {
    return Keychain.getToken()
}
let authPlugin = AccessTokenPlugin(tokenClosure: tokenClosure)

Xcode version: 9.4.1

@sunshinejr

I finally solved this issue by using code below, hope it could help who are facing the same

var tokenClosure: String {
    return Keychain.getToken()
}
let authPlugin = AccessTokenPlugin(tokenClosure: tokenClosure)
Was this page helpful?
0 / 5 - 0 ratings