firebase-ios-sdk: Firebase ID token has expired.

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 11.7
  • Firebase SDK version: 6.31.0
  • Firebase Component: Auth
  • Component version: 6.31.0
  • Installation method: Carthage

[REQUIRED] Step 2: Describe the problem

Steps to reproduce:

In our app, we use Firebase authentication token to identify user on our backend. We are sending this token with every request. We retrieve Firebase token using getIDToken(completion:) with every request.

The documentation says the method Retrieves the Firebase authentication token, possibly refreshing it if it has expired., but from time to time, we receive error Firebase ID token has expired. Get a fresh ID token from your client app and try again (auth/id-token-expired). See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token. This is kind of a major issue for our app, as we automatically log out users which receive unauthenticated error (because of user deletion functionality) - not mentioning that all of the requests automatically fail because of expired token.

Am I misunderstanding the concept of retrieving the token using getIDToken(completion:), or is this the bug in SDK?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 31 (3 by maintainers)

Most upvoted comments

I’m experiencing this same issue with the javascript SDK. In my SPA, I can reliably reproduce this in the following manner:

  1. Authenticate via Firebase, everything will work normally
  2. Leave the computer for a while (Mac in my case), so that it goes into Sleep mode (or manually put it into Sleep I guess). Let it sleep for a while (an hour or so, so that the last retrieved token will have expired)
  3. Wake up the computer, and resume using the app. For some reason, firebase.auth().currentUser.getIdToken() will now still continue to serve the previous token, which has now expired.

I realize this isn’t related to the iOS SDK specifically, but I wanted to chime in, in case the issue isn’t unique to this specific SDK.

Chatted with the team a bit-- one thing to consider is that the SDK will automatically refresh the token if it has expired or if it will expire in the next 5 minutes. If the device has its clock running a bit behind then it will think the token has not expired even though it may have. Then, when you send it to your backend, your backend has the correct time and sees the token as expired.

The reverse may be true as well: if for some reason your server’s clock is a bit ahead, it may incorrectly think the token is expired.

One thing to try: you can manually parse the ID token and log failures. Here’s a snippet in Javascript but you can apply this equally in any language:

// Where atob is Base64 decode
const expirationTimeInSecondsSinceEpoch =
    JSON.parse(atob(token.split('.')[1])).exp

That will get you the expiration time of the token in question. I’d suggest adding some sort of logging to your backend that records the token’s expiration time and the server’s current time when you catch this particular error.

(And I have to post this just in case, please don’t post the ID token or other information here 😄 )

For the folks seeing this in the JS SDK, please file a separate issue in the JS SDK repo: https://github.com/firebase/firebase-js-sdk/issues

Also seeing this issue in the JS SDK about 20 times a day.

Hi @rosalyntan, additional logging is great but I am not sure if this method is the one to take. Not sure that this issue is easily reproducible in debug environment 😬

To get a token, I’m calling Auth.auth().currentUser.getIDToken(completion:). To verify the token locally, I followed the same path that the backend uses (firebase-admin-sdk via jsonwebtoken npm package).

  1. Split the JWT into it’s various parts
  2. Find the payload part
  3. base64 decode it
  4. Parse it into JSON
  5. Extract the value from the “exp” key as a number
  6. Compare current date’s timeIntervalSince1970 with expires at date
    func verifyIDToken(token: String) throws -> Bool {
        let parts = token.split(separator: ".")
        // JSON Web Token should have a header, payload, and signature
        guard parts.count == 3 else {
            throw NSError(domain: "com.glasswing.cocoon", code: 500, userInfo: [
                NSLocalizedDescriptionKey: "Token has wrong number of parts. Expected 3, got \(parts.count)"
            ])
        }
        var payload = String(parts[1])
        // Base64 strings need to be divisible by 4 to be decoded with Data, otherwise pad with '='
        // https://stackoverflow.com/questions/4080988/why-does-base64-encoding-require-padding-if-the-input-length-is-not-divisible-by
        while payload.utf8.count % 4 != 0 {
            payload.append("=")
        }
        // Payload is base64 encoded json
        guard let payloadData = Data(base64Encoded: payload, options: .ignoreUnknownCharacters) else {
            throw NSError(domain: "com.glasswing.cocoon", code: 500, userInfo: [
                NSLocalizedDescriptionKey: "Token payload could not be decoded from base64 to json string"
            ])
        }
        guard let parsed = try? JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any] else {
            throw NSError(domain: "com.glasswing.cocoon", code: 500, userInfo: [
                NSLocalizedDescriptionKey: "Token payload json could not be parsed"
            ])
        }
        // Expiration date is "exp" field
        guard let exp = parsed["exp"] as? Int else {
            throw NSError(domain: "com.glasswing.cocoon", code: 500, userInfo: [
                NSLocalizedDescriptionKey: "Could not find value for `exp` key in token payload"
            ])
        }
        let isExpired = Date().timeIntervalSince1970 >= TimeInterval(exp)
        if isExpired {
            // Log an error to Sentry to track instances of this
            let error = NSError(domain: "com.glasswing.cocoon", code: 500, userInfo: [
                NSLocalizedDescriptionKey: "Expired token received from FIRAuth",
                "expiredAt": Date(timeIntervalSince1970: TimeInterval(exp)),
                "currentTime": Date()
            ])
            AnalyticsManager.shared.trackError(error: error)
        }
        return !isExpired
    }