firebase-ios-sdk icon indicating copy to clipboard operation
firebase-ios-sdk copied to clipboard

Synchronously retrieve an user's ID token

Open emilioschepis opened this issue 4 years ago • 19 comments

[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)")

emilioschepis avatar Jul 02 '20 08:07 emilioschepis

I've tracked this as a feature request internally as b/160627142. @rosalyntan may have some ideas why your workaround doesn't work.

yuchenshi avatar Jul 06 '20 20:07 yuchenshi

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 avatar Aug 13 '20 06:08 musaprg

@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!

emilioschepis avatar Aug 13 '20 07:08 emilioschepis

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?

rosalyntan avatar Aug 29 '20 00:08 rosalyntan

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.

emilioschepis avatar Aug 29 '20 05:08 emilioschepis

+1 Having the same issue.

daihovey avatar Sep 01 '20 14:09 daihovey

@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 avatar Sep 02 '20 01:09 daihovey

@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.

emilioschepis avatar Sep 02 '20 08:09 emilioschepis

@rosalyntan any update you can share regarding this issue?

daihovey avatar Oct 12 '20 03:10 daihovey

I think I'm in the same boat. Apollo GraphQL back end.

tlester avatar Sep 25 '21 11:09 tlester

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.

tlester avatar Sep 25 '21 12:09 tlester

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)
            }
            
            
        }
}


tlester avatar Sep 25 '21 12:09 tlester

Any update on this? This is really annoying behavior and basically eliminates using semaphores with Firebase at all

LilaQ avatar Aug 03 '22 22:08 LilaQ

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 avatar Aug 04 '22 06:08 peterfriese

@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 avatar Aug 04 '22 14:08 LilaQ

@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 avatar Aug 04 '22 14:08 peterfriese

@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.

LilaQ avatar Aug 04 '22 14:08 LilaQ

Understood. I suggested this approach as it is available now, and we expect async/await to see more adoption going forward.

peterfriese avatar Aug 04 '22 15:08 peterfriese