I'm currently trying to set up token authentication with the Apollo iOS library. I've looked through the docs here which suggests the following code:
extension Network: HTTPNetworkTransportPreflightDelegate {
func networkTransport(_ networkTransport: HTTPNetworkTransport,
willSend request: inout URLRequest) {
// Get the existing headers, or create new ones if they're nil
var headers = request.allHTTPHeaderFields ?? [String: String]()
// Add any new headers you need
headers["Authorization"] = "Bearer \(UserManager.shared.currentAuthToken)"
// Re-assign the updated headers to the request.
request.allHTTPHeaderFields = headers
Logger.log(.debug, "Outgoing request: \(request)")
}
}
The issue I'm having is that my currentAuthToken method requires a callback since it's asynchronous and in the example given the code is synchronous.
Right now, my "hacky" workaround is to do something like this:
class Network {
// Other irrelevant stuff
var accessToken: String?
func query<Query: GraphQLQuery>(_ query: Query,
cachePolicy: CachePolicy = .returnCacheDataElseFetch,
queue: DispatchQueue = .main,
handler: GraphQLResultHandler<Query.Data>? = nil
) {
// The asynchronous call
Authenticator.shared.getAccessToken { [weak self] (token) in
self?.accessToken = token
self?.apollo.fetch(query: query, cachePolicy: cachePolicy, queue: queue, resultHandler: handler)
}
}
which runs the getAccessToken in a custom wrapper around the ApolloClient.fetch(query) method and sets it to an instance variable which is then accessed here:
func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
var headers = request.allHTTPHeaderFields ?? [String: String]()
headers["Authorization"] = "Bearer \(accessToken ?? "")"
request.allHTTPHeaderFields = headers
log.debug("Sending request with \(accessToken ?? "no token")")
log.debug("Outgoing request: \(request)")
}
I'm not sure if this is the best solution, but I have tried it so far and it works as expected. If there could be any clarity regarding this that would be great.
The same issue goes for setting up subscriptions as the magicToken code is again synchronous and I'm sure I'm not the only person who would be working with an async token-getter function
private lazy var webSocketTransport: WebSocketTransport = {
let url = URL(string: "ws://localhost:8080/websocket")!
let request = URLRequest(url: url)
let authPayload = ["authToken": magicToken]
return WebSocketTransport(request: request, connectingPayload: authPayload)
}()
Sorry I hadn't taken the time to search before opening this- https://github.com/apollographql/apollo-ios/issues/834#issuecomment-542442930
I looked over at the other issue and I still wasn't able to get it solved. My sync function looks like this:
func getAccessTokenSync() -> String? {
let semaphore = DispatchSemaphore(value: 0)
var authToken: String?
DispatchQueue.global(qos: .background).async {
self.getAccessToken { (token) in
authToken = token
semaphore.signal()
}
print(authToken)
semaphore.wait()
}
return authToken
}
but the authToken returned is always nil. When I don't run on the background thread the app blocks
Calling it here:
func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
var headers = request.allHTTPHeaderFields ?? [String: String]()
let token = Authenticator.shared.getAccessTokenSync() ?? ""
headers["Authorization"] = "Bearer \(token)"
request.allHTTPHeaderFields = headers
log.debug("Sending request with token \(token)")
log.debug("Outgoing request: \(request)")
}
For the web socket issue we added a way to update the headers in 0.28.0 via #1224, but you also could do the call to the async API before starting the setup of your web socket transport. For example:
func setupWebSocket(with magicToken: String) -> WebSocketTransport {
// existing code in your lazy getter
}
self.getAccessToken() { token in
let webSocket = self.setupWebSocket(with: token)
// proceed with setting up apollo client
}
It looks like with your code in this comment you're doing an additional dispatch to a background queue that never calls back to the main queue, so the `wait() may be getting called after the initial method returns.
I think if you just ditch the DispatchQueue.global bit, it should still work.
Hey, so I have this setup currently:
extension Network: HTTPNetworkTransportPreflightDelegate {
func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool {
return true
}
func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
let semaphore = DispatchSemaphore(value: 0)
var authToken: String?
Authenticator.shared.getAccessToken { (token) in
authToken = token
semaphore.signal()
}
print(authToken)
semaphore.wait()
if let token = authToken {
var headers = request.allHTTPHeaderFields ?? [String: String]()
headers["Authorization"] = "Bearer \(token)"
request.allHTTPHeaderFields = headers
}
log.debug("Sending request with token \(authToken ?? "no token")")
log.debug("Outgoing request: \(request)")
}
}
however, the callback function is never called for some reason which means the semaphore.signal() is never called either causing the UI thread to be blocked indefinitely.
When I remove the DispatchSemaphore code, the callback is reached but it's not assigned to local variable making it absent in the request
Ahhh ok that's why you added the Dispatch - even though getAccessToken is async, it appears to still be using the same thread.
What about going back to using the Dispatch, but putting semaphore.wait outside the dispatch queue:
func getAccessTokenSync() -> String? {
let semaphore = DispatchSemaphore(value: 0)
var authToken: String?
DispatchQueue.global(qos: .background).async {
self.getAccessToken { (token) in
authToken = token
semaphore.signal()
}
}
semaphore.wait()
print(authToken)
return authToken
}
that starts the wait on the thread before the return, so that it has to get the signal from the background thread before it proceeds, but since the access token is being retrieved on a different thread, it shouldn't be blocked by the wait.
Ahhh ok that's why you added the
Dispatch- even thoughgetAccessTokenis async, it appears to still be using the same thread.What about going back to using the
Dispatch, but puttingsemaphore.waitoutside the dispatch queue:func getAccessTokenSync() -> String? { let semaphore = DispatchSemaphore(value: 0) var authToken: String? DispatchQueue.global(qos: .background).async { self.getAccessToken { (token) in authToken = token semaphore.signal() } } semaphore.wait() print(authToken) return authToken }that starts the
waiton the thread before thereturn, so that it has to get the signal from the background thread before it proceeds, but since the access token is being retrieved on a different thread, it shouldn't be blocked by thewait.
So far from my basic testing, it鈥檚 worked like a charm. I鈥檒l take a more detailed look at it tomorrow but it works as expected.
I think adding in a callback to the willSend function in later version of the client would be a good idea making common implementations like this simpler.
I鈥檒l probably try to cache the result to prevent having to make that call every time since the value won鈥檛 be changing often.
We're going to be working on changes to the networking stack later in the year that will (hopefully) make this and any other async operation a hell of a lot easier.
That said, I think we've addressed your question for the time being, do you mind if we close this issue out?
Thank you for your help!
You're welcome!
Most helpful comment
We're going to be working on changes to the networking stack later in the year that will (hopefully) make this and any other async operation a hell of a lot easier.
That said, I think we've addressed your question for the time being, do you mind if we close this issue out?