Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MFA TOTP resolveSignIn fails with "user._updateTokensIfNecessary is not a function" error #8647

Open
kitoko552 opened this issue Nov 21, 2024 · 3 comments

Comments

@kitoko552
Copy link

Operating System

macOS Ventura 13.6.6

Environment (if applicable)

Node.js v20.1.0 Chrome 128

Firebase SDK Version

v9.22.0 (still reproducible in the latest version v11.0.2)

Firebase SDK Product(s)

Auth

Project Tooling

Next.js app with React and Jest for testing. Built using TypeScript.

Detailed Problem Description

When attempting to reauthenticate a user with MFA (TOTP) enabled, the following error occurs:

What I was trying to achieve:

  • Reauthenticate a user using Google sign-in
  • Handle MFA_REQUIRED error by resolving with TOTP code

What actually happened:

  • The initial reauthentication throws MFA_REQUIRED as expected
  • When calling mfaResolver.resolveSignIn() with TOTP assertion, it fails with an internal error

Error message:

user._updateTokensIfNecessary is not a function
    at UserCredentialImpl._forOperation
    at MultiFactorResolverImpl.eval [as signInResolver]
    at ...(my functions)

This appears to be an internal Firebase Auth SDK issue where the user object is missing an expected method during the MFA resolution process.

Steps and code to reproduce issue

const auth = getAuth()
try {
  await reauthenticateWithPopup(auth.currentUser!, new GoogleAuthProvider())
} catch (error) {
  const fbError = error as FirebaseError
  if (fbError.code === AuthErrorCodes.MFA_REQUIRED) {
    const mfaResolver = getMultiFactorResolver(auth, fbError)
    const totpInfo = mfaResolver.hints.find(
      (info) => info.factorId === FactorId.TOTP,
    )
    if (!totpInfo) {
      throw new Error('TOTP is not enrolled')
    }

    // user inputs otp

    const multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn(
      totpInfo.uid,
      otp,
    )
    
    // fails with the error
    await mfaResolver.resolveSignIn(multiFactorAssertion)
  }
}
@kitoko552 kitoko552 added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Nov 21, 2024
@google-oss-bot
Copy link
Contributor

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@jbalidiong jbalidiong added api: auth needs-attention and removed needs-triage new A new issue that hasn't be categoirzed as question, bug or feature request labels Nov 21, 2024
@hsubox76
Copy link
Contributor

I wasn't able to reproduce this using the same code. Can you log the value of user in a couple of places:

  1. Can you log auth.currentUser right above await reauthenticateWithPopup(auth.currentUser!, new GoogleAuthProvider() (and also log auth.currentUser._updateTokensIfNecessary)
  2. Can you log error.user at the top of the catch block? (and also log error.user_updateTokensIfNecessary)
  3. And maybe auth.currentUser right above the failing line (await mfaResolver.resolveSignIn(multiFactorAssertion))? (and also log auth.currentUser._updateTokensIfNecessary)

I know this is a code snippet so you've probably already accounted for this elsewhere, but are you doing an await auth.authStateReady() somewhere before this or putting this inside onAuthStateChanged() in order to guarantee that auth.currentUser is populated when this code runs?

@kitoko552
Copy link
Author

@hsubox76 Thank you for your response. Let me provide more context about the actual flow - the reauthentication is triggered when trying to unenroll TOTP MFA, which results in a CREDENTIAL_TOO_OLD_LOGIN_AGAIN error. Here's the more complete code snippet that might be relevant to this issue:

async function unenroll() {
  const user = getCurrentUser()
  const mfaInfo = multiFactor(user).enrolledFactors.find(
    (factor) => factor.factorId === FactorId.TOTP,
  )
  if (!mfaInfo) {
    throw new Error('TOTP is not enrolled')
  }

  try {
    await multiFactor(user).unenroll(mfaInfo)
  } catch (error) {
    const firebaseError = error as FirebaseError
    if (firebaseError.code === AuthErrorCodes.CREDENTIAL_TOO_OLD_LOGIN_AGAIN) {
      await reauthenticate(user)
    }
  }
}

async function reauthenticate(user: User) {
  try {
    await reauthenticateWithPopup(user, new GoogleAuthProvider())
  } catch (error) {
    const fbError = error as FirebaseError
    if (fbError.code === AuthErrorCodes.MFA_REQUIRED) {
      const mfaResolver = getMultiFactorResolver(auth, fbError)
      const totpInfo = mfaResolver.hints.find(
        (info) => info.factorId === FactorId.TOTP,
      )
      if (!totpInfo) {
        throw new Error('TOTP is not enrolled')
      }

      // user inputs otp

      const multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn(
        totpInfo.uid,
        otp,
      )
      
      // fails with the error
      await mfaResolver.resolveSignIn(multiFactorAssertion)
    }
  }
}

// when user wants to unenroll TOTP MFA
unenroll()

are you doing an await auth.authStateReady() somewhere before this or putting this inside onAuthStateChanged() in order to guarantee that auth.currentUser is populated when this code runs?

Regarding this question, yes, we are using onAuthStateChanged() to ensure currentUser is properly populated.

As for the user logging points you requested, I'll gather that information and update this thread shortly.

By the way, I found that using signInWithPopup instead of reauthenticateWithPopup works without the error and successfully completes the reauthentication. Here's the working code for reference:

async function reauthenticate(user: User) {
  try {
    // await reauthenticateWithPopup(user, new GoogleAuthProvider())
    await signInWithPopup(getAuth(app), new GoogleAuthProvider())
  } catch (error) {
    // ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants