I use Firebase and getIDToken method is asynchronous so I cannot use AccessTokenPlugin with tokenClosure.
Instead I implemented a request closure:
let requestClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider.RequestResultClosure) in
do {
var request = try endpoint.urlRequest()
// TODO: sign only specific endpoints and leave other unsigned (how?)
if shouldSignEndpoint {
Auth.auth().currentUser?.getIDToken { token, error in
if let token = token {
request.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
done(.success(request))
}
}
} else {
done(.success(request))
}
} catch {
done(.failure(MoyaError.requestMapping(error.localizedDescription)))
}
}
The problem is that some endpoints are public, so I do not need to add 'Authorization' header. But in the requestClosure I see no way to decide which endpoints are public and which are not.
What is the best way to specify which endpoints are needed to sign and which are not?
@oluckyman You probably want to create a custom PluginType for this. You can鈥檛 really do this using the requestClosure because the underlying TargetType has been abstracted away at this point.
Sent with GitHawk
This issue has been marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Hi @oluckyman
Maybe this will help you.
Instead of setting the Authorization header inside the requestClosure, you could just save the token on a property for example, and then use the AccessTokenPlugin to set the header.
Take a look at this file, where I request and save the token, using the requestClosure https://github.com/pietrocaselani/Trakt-Swift/blob/master/Trakt/TraktTokenInterceptor.swift
And here I use the AceessTokenPlugin to set the header https://github.com/pietrocaselani/Trakt-Swift/blob/master/Trakt/Trakt.swift
Let me know if this solve your problem or if you need any further help.
Hi @pietrocaselani I'm pretty new in iOS development, so your code is too complex for me. As I understood, you're getting token from response and save it in a property.
In my case token is requested from Firebase Auth module asynchronously . And to be sure that the token is valid I have to retrieve it right before request.
@oluckyman You probably want to chain your Firebase auth token request with your requests from Moya provider then. You would make your auth request, save the token in a local variable, then execute your Moya request on completion
@oluckyman something like this should work. I didn't test this code, but it's based on the code I posted before.
class MyAPIClient {
private var token: String?
private func isTokenValid() -> Bool {
return token != nil
}
func createMoyaProvider<T: TargetType>(target: T.Type) -> MoyaProvider<T> {
let requestClosure = makeRequestClosure(target: target)
let accessTokenPlugin = AccessTokenPlugin(tokenClosure: self.token ?? ""))
let plugins = [accessTokenPlugin]
return MoyaProvider<T>(requestClosure: requestClosure, plugins: plugins)
}
private func makeRequestClosure<T: TargetType>(target: T.Type) -> MoyaProvider<T>.RequestClosure {
let requestClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider.RequestResultClosure) in
do {
var request = try endpoint.urlRequest()
if isTokenValid() {
done(.success(request))
} else {
Auth.auth().currentUser?.getIDToken { newToken, error in
self.token = newToken
done(.success(request))
}
}
} catch {
done(.failure(MoyaError.requestMapping(error.localizedDescription)))
}
}
return requestClosure
}
}
@SD10 that's good idea. This way moya api layer will assume that the token always exists and valid and I can use AccessTokenPlugin.
Do I understand right, that I have to implement a wrapper around moya api? That wrapper should decide if the resource is public or not and if it's public then just call moya api, and if not then request token, wait for token and call moya api after that.
Probably that wrapper could reuse AccessTokenAuthorizable extension to not duplicate public/non-public routes definition.
@pietrocaselani
Thank you! But in my case token could be invalid even if it exists. It can be expired. So the only way to be sure that it's valid is to request it before any non-public request.
Your code snippet still could work for me. I'll just assume that token is always invalid.
But it brings us where we started: I do not want to request token if it is not needed for request. And in requestClosure I have no way to check if the endpoint is public or not.
"I do not want to request token if it is not needed for request. And in
requestClosureI have no way to check if the endpoint public or not."
You only need to request a new token when the token is expired. You will still have to check if the token is valid (is expired or not) in every request, inside the requestClosure, but you don't have to request a new token every single time.
@pietrocaselani True. I understand. But still, how to deal with public endpoints which do not require Authorization header at all?
@oluckyman What we do is to create our own protocol that has auth type, if you are using only bearer or nothing, you would have:
protocol AuthTargetType: TargetType {
var auth: Bool { get }
}
And then in your wrapper you would check if you need to do auth or not. Also, your wrapper could be a little bit smarter by checking the current token (if it expired or not), then:
Just be careful to not get into infinity loop with retries, we usually retry with these steps to max 3 times and then activate the request once the network connection is up again.
@oluckyman I think if you implement the protocol AccessTokenAuthorizable on your target type and then use the AccessTokenPlugin, you will get what you want.
Example:
enum MyAPIEndpoint {
case somePublicEndpoint
case somePrivateEndpoint
}
extension MyAPIEndpoint: TargetType, AccessTokenAuthorizable {
// implement baseURL, path, method, task...
var authorizationType: AuthorizationType {
switch self {
case .somePublicEndpoint: return .none
case .somePrivateEndpoint: return .bearer // or .basic
}
}
}
Then AccessTokenPlugin will check the AuthorizationType and add the header or not.
@pietrocaselani this is how I tried to do it initially. The problem was that AccessTokenPlugin assumes that token is stored as a property, but in my case I have to do async call to get token before request.
Seems like requestClosure (as you described above) does not work together with AccessTokenPlugin. Because requestClosure have no access to TargetType and AccessTokenPlugin unable do async request.
@sunshinejr I thought there will be out-of-the-box solution, since my use case is pretty common. Thanks for explanation! Now I understand, that I'll need a wrapper around moya
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 馃憤.
I had the same problem than @oluckyman, since the call to get the token is asynchronous and you cannot store it in order garantee you always have an updated token (Firebase suggests also this) you need to make this async call every time you wanna make a request.
My solution was to create a protocol which inherits from TargetType:
protocol FirebaseAuthorizableTargetType: TargetType {}
and I have a MoyaProviderFactory:
class MoyaProviderFactory {
private init() {}
//shared plugins and stuff
static func make<T: TargetType>(_: T.Type) -> MoyaProvider<T> {
return MoyaProvider<T>(/* your params */)
}
static func makeAuthorizable<T: FirebaseAuthorizableTargetType>(_: T.Type, firebaseAuth: FirebaseAuthType = Auth.auth()) -> MoyaProvider<T> {
let requestClosure = { (endpoint: Endpoint, closure: @escaping MoyaProvider.RequestResultClosure) in
do {
var urlRequest = try endpoint.urlRequest()
if let cu = firebaseAuth.currentUser {
cu.getIDTokenForcingRefresh(true) { (token, error) in
if let t = token {
urlRequest.setValue("Bearer \(t)", forHTTPHeaderField: "authorization")
closure(.success(urlRequest))
} else {
closure(.failure(.underlying(/* your custom error */, nil)))
}
}
} else {
closure(.failure(.underlying(/* your custom error when there is no user */, nil)))
}
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
return MoyaProvider<T>(requestClosure: requestClosure, /* your other params */)
}
}
so on the caller site, every time I want to create a MoyaProvider I call:
let foo = MoyaProviderFactory.make(NonFirebaseAuthorizableTarget.self) // for public request
let bar = MoyaProviderFactory.makeAuthorizable(FirebaseAuthorizableTarget.self) // for non-public request
Notes:
FirebaseAuthorizableTargetType) with no requirements is a good idea, in terms of semantic, but I couldnt come up with a better and clean solutionFirebaseAuthType so we can create testable providers
Most helpful comment
I had the same problem than @oluckyman, since the call to get the token is asynchronous and you cannot store it in order garantee you always have an updated token (Firebase suggests also this) you need to make this async call every time you wanna make a request.
My solution was to create a protocol which inherits from
TargetType:and I have a
MoyaProviderFactory:so on the caller site, every time I want to create a
MoyaProviderI call:Notes:
FirebaseAuthorizableTargetType) with no requirements is a good idea, in terms of semantic, but I couldnt come up with a better and clean solutionFirebaseAuthTypeso we can create testable providers