firebase-ios-sdk
firebase-ios-sdk copied to clipboard
Synchronously retrieve an user's ID token
[REQUIRED] Step 1: Describe your environment
- Xcode version: 12.0 Beta
- Firebase SDK version: 6.27.0
- Firebase Component: Auth (Auth, Core, Database, Firestore, Messaging, Storage, etc)
- Component version: 6.27.0 (Firebase/Auth), 6.6.0 (FirebaseAuth)
- Installation method:
CocoaPods
[REQUIRED] Step 2: Describe the problem
Steps to reproduce:
Can not synchronize the retrieval of an ID token using a DispatchSemaphore
.
Calling Firebase.User.getIDToken
and awaiting its result using a semaphore results in the closure never getting called.
If the semaphore is given a timeout, the closure completes (almost) immediately after the timeout is over, suggesting that the semaphore is blocking the getIDToken
call, too.
I have also tried wrapping the call in a global dispatch queue, with the same result. I have provided the simpler version below.
How can I write the token-retrieving code in a synchronous way?
Relevant Code:
let semaphore = DispatchSemaphore(value: 0)
var token: String?
Auth.auth().currentUser?.getIDToken { firebaseToken, error in
token = firebaseToken
semaphore.signal()
}
semaphore.wait()
print("Token is \(token)")
I've tracked this as a feature request internally as b/160627142. @rosalyntan may have some ideas why your workaround doesn't work.
I also encountered the same situation, and I want to use the implementation like this in our product.
I'm not sure, but I think that's due to the thread-safety model of Auth. getIDToken
, internally getIDTokenWithCompletion
pushes the passed completion block to dispatch_main_queue
. This causes dead-locking at semaphore.wait()
on the main-thread.
https://github.com/firebase/firebase-ios-sdk/blob/b449bee0a2291ae97ab0ef0bc1452cbe873a6443/FirebaseAuth/Sources/User/FIRUser.m#L844-L861
ref: https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseAuth/Docs/threading.md
@musaprg I also noticed the same thing in the source code, that absolutely seems to be the cause of the deadlock.
@rosalyntan can you suggest a workaround to this issue? I understand that this feature request might not be high priority for the Firebase team right now but a temporary solution, if it exists, would be much appreciated. Thanks!
Hi @emilioschepis, thanks for filing this issue! Firebase Auth APIs are asynchronous by design, and blocking the main thread to wait for the completion of getIDToken
is not advised. May I ask what you are trying to accomplish by synchronizing the retrieval of the ID token? Can you achieve what you want by putting whatever is waiting for the semaphore into the completion block instead?
Hi @rosalyntan, here is my use case:
I am using the Apollo iOS library to make GraphQL requests to a server, and I send the user's current Firebase JWT token to authenticate.
As you can see from the docs, headers for each request are defined in a delegate method.
Unfortunately this method does not have a completion block; instead it directly changes an inout
parameter on the request itself. For this reason, I need to block the thread through a semaphore until the token is available so that I can use it inside the request.
Is there a way to achieve the same result while respecting Firebase Auth's asynchronous approach?
Thanks.
+1 Having the same issue.
@emilioschepis A (terrible) internal workaround (we haven't launched yet) is to save the access token locally per session on login, rather than checking every request - hoping we can find a better approach soon.
@daihovey thanks for the tip. I'm still in the prototyping phase and my backend database (Hasura) supports a combination of admin secret and role so I can simulate being any user I need. As you said, however, these workarounds won't be secure enough for production so let's hope an official fix or workaround will come soon.
@rosalyntan any update you can share regarding this issue?
I think I'm in the same boat. Apollo GraphQL back end.
Has anyone come up with reliable solutions for this? It's been a year since this issue was created. This is a pretty major show stopper.
I GOT MINE TO WORK. here's my solution (for Apollo GraphQL Client):
//
// Apollo.swift
// Swingist
//
// Created by Thomas Lester on 9/23/21.
//
import Foundation
import Apollo
import FirebaseAuth
class Network {
static let shared = Network()
private(set) lazy var apollo: ApolloClient = {
let cache = InMemoryNormalizedCache()
let store1 = ApolloStore(cache: cache)
let authPayloads = ["Authorization": "Bearer token"]
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = authPayloads
let client1 = URLSessionClient(sessionConfiguration: configuration, callbackQueue: nil)
let provider = NetworkInterceptorProvider(client: client1, shouldInvalidateClientOnDeinit: true, store: store1)
let url = URL(string: "https://swingist.com/graph")!
let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url)
return ApolloClient(networkTransport: requestChainTransport,
store: store1)
}()
}
class NetworkInterceptorProvider: DefaultInterceptorProvider {
override func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
var interceptors = super.interceptors(for: operation)
interceptors.insert(CustomInterceptor(), at: 0)
return interceptors
}
}
class CustomInterceptor: ApolloInterceptor {
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Swift.Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
Auth.auth().currentUser?.getIDToken() {idToken, error in
if let error = error {
print("Error: \(error)")
}
request.addHeader(name: "Authorization", value: "Bearer \(idToken!)")
print("request :\(request)")
print("response :\(String(describing: response))")
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
}
}
Any update on this? This is really annoying behavior and basically eliminates using semaphores with Firebase at all
If you can use async/await in your app, you can call this method asynchronously:
func refreshIDToken() {
Task {
do {
idToken = try await user?.idTokenForcingRefresh(true) ?? ""
}
catch {
errorMessage = error.localizedDescription
print(error)
}
}
}
@peterfriese This still deadlocks on semaphores.
let syncSemaphore = DispatchSemaphore(value: 0)
Task {
try! await Auth.auth().currentUser?.idTokenForcingRefresh(true)
syncSemaphore.signal()
}
syncSemaphore.wait()
print("I'm never reached")
@LilaQ my suggestion was, can you get rid of semaphores and replace them with async/await?
Task {
do {
idToken = try await user?.idTokenForcingRefresh(true) ?? ""
makeCallThatRequiresIDToken(idToken)
}
catch {
errorMessage = error.localizedDescription
print(error)
}
}
@peterfriese That would basically call for an entire refactor of my code, since everything is laid out for semaphores for syncing. The only thing that's basically ruining it now, is getting the token, which is supposed to happen on every web-request, so it can get a new one if necessary and then just perform the authorized request.
Understood. I suggested this approach as it is available now, and we expect async/await to see more adoption going forward.