firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.47k stars 1.43k forks source link

Synchronously retrieve an user's ID token #5964

Open emilioschepis opened 4 years ago

emilioschepis commented 4 years ago

[REQUIRED] Step 1: Describe your environment

[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)")
yuchenshi commented 4 years ago

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

musaprg commented 3 years ago

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

emilioschepis commented 3 years ago

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

rosalyntan commented 3 years ago

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?

emilioschepis commented 3 years ago

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.

daihovey commented 3 years ago

+1 Having the same issue.

daihovey commented 3 years ago

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

emilioschepis commented 3 years ago

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

daihovey commented 3 years ago

@rosalyntan any update you can share regarding this issue?

tlester commented 2 years ago

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

tlester commented 2 years ago

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 commented 2 years ago

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

        }
}
LilaQ commented 1 year ago

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

peterfriese commented 1 year ago

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)
      }
    }
  }
LilaQ commented 1 year ago

@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")
peterfriese commented 1 year ago

@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)
  }
}
LilaQ commented 1 year ago

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

peterfriese commented 1 year ago

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

danielepantaleone commented 4 weeks ago

Hi,

I'm maintaining a proprietary framework that needs to retrieve the Firebase ID Token in a synchronous environment, and I stumbled upon this issue. I can't refactor my whole codebase to add swift concurrency support because I have several services that needs to be synchronised with each other. I decided to make a swift framework to help calling asynchronous code in a synchronous way: https://github.com/danielepantaleone/Resyncer

Usage:

let resyncer = Resyncer()
let idToken = try resyncer.synchronize {
    try await Auth.auth().currentUser?.getIDToken()
}
// continue with token usage

or with callbacks if preferred:

let resyncer = Resyncer()
let idToken = try resyncer.synchronize { callback
    Auth.auth().currentUser?.getIDToken { token, error in
        if let error {
            callback(.failure(error))
        } else {
            callback(.success(token))
        }
    }
}
// continue with token usage

It uses a condition variable to block the calling thread waiting for asynchronous code to terminate (so don't use it from the main thread) and will offload asynchronous work to a separate thread.

I'm posting it here, maybe it will help someone else 😉 (even though I'm a bit late to the party 😁)