iosswiftfirebasefirebase-authentication

Firebase in iOS: Assessing the Need for Manual Token Refreshing


Currently, I am using the following code in my iOS client to determine whether we need to present a login screen:

if Auth.auth().currentUser == nil

Here is the login screen’s logic:

      @objc func handleAppleSignUp() {
          Analytics.logEvent("handleAppleSignUp", parameters: nil)
          
          appleSignUpButton?.stopPulseAnimation()
          
          startSignInWithAppleFlow()
      }

      //
      // https://firebase.google.com/docs/auth/ios/apple
      //
      
      @available(iOS 13, *)
      func startSignInWithAppleFlow() {
        let nonce = randomNonceString()
        currentNonce = nonce
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = sha256(nonce)

        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
      }
      
      private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        var randomBytes = [UInt8](repeating: 0, count: length)
        let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
        if errorCode != errSecSuccess {
          fatalError(
            "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
          )
        }

        let charset: [Character] =
          Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")

        let nonce = randomBytes.map { byte in
          // Pick a random character from the set, wrapping around if needed.
          charset[Int(byte) % charset.count]
        }

        return String(nonce)
      }

      @available(iOS 13, *)
      private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
          String(format: "%02x", $0)
        }.joined()

        return hashString
      }
  }

  // https://fluffy.es/sign-in-with-apple-tutorial-ios/
  extension LoginViewController:  ASAuthorizationControllerPresentationContextProviding {
      func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
          // Return the window of the current view controller
          return self.view.window!
      }
  }

  extension LoginViewController: ASAuthorizationControllerDelegate {
      func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
          guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
          }
          guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
          }
          guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
          }
          // Initialize a Firebase credential, including the user's full name.
          let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
                                                            rawNonce: nonce,
                                                            fullName: appleIDCredential.fullName)
            
          EmulatorUtils.authUseEmulatorIfPossible()
          
          // Sign in with Firebase.
          Auth.auth().signIn(with: credential) { (authResult, error) in
            if let error = error {
              // Error. If error.code == .MissingOrInvalidNonce, make sure
              // you're sending the SHA256-hashed nonce as a hex string with
              // your request to Apple.
              print(error.localizedDescription)
              return
            }
            // User is signed in to Firebase with Apple.
            // ...
              
              Analytics.logEvent("sign_in_success", parameters: nil)
              
              self.delegate?.updateBasedOnLoginStatus()
          }
        }
      }

      func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
      }
  }

I was wondering: do we ever need to handle login token refreshing manually? Some of my users have reported that interactions with Firebase Functions and Firestore sometimes fail. In each case, this issue is resolved by logging out and then logging back in.

If I do need to handle login token refreshing manually, could someone explain how and when to do so?


Solution

  • do we ever need to handle login token refreshing manually?

    If you're using the Firebase client libraries for any supported platform, this happens automatically. You don't have to do anything at all.

    If the only way you're checking the user status is with currentUser, then you're probably doing it wrong. Your application should be using an auth state observer to know for sure when a user is signed in or signed out. currentUser doesn't handle the case on first launch before the SDK has time to validate the user's token. When your app is freshly launched, currentUser will always be nil, before the SDK has a chance to refresh any prior token as needed, but the observer will let you know when that happens, and you can update the UI of your app to reflect that.

    Also read: Why is my currentUser == null?