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

signInWithRedirect stops working following login from a second tab on Macs using Chrome #7037

Closed
TheNotary opened this issue Feb 14, 2023 · 8 comments
Assignees

Comments

@TheNotary
Copy link

TheNotary commented Feb 14, 2023

[REQUIRED] Describe your environment

  • Operating System version: macOS Monterey
  • Browser version: Chrome 109.0.5414.87 (Official Build) (x86_64) (the update circle just spins, so I guess this is my version forever), also Firefox 110.0 doesn't work, but for different reasons I believe
  • Firebase SDK version: 9.17.1
  • Firebase Product: Auth

Note: windows machines running chrome are strangely unaffected by this bug... Old Safari 15.6.1 seems to work fine, though 16.1+ reportedly breaks.

[REQUIRED] Describe the problem

Steps to reproduce:

  1. Sign in at localhost (using a Microsoft OAuth Provider and signInWithRedirect),
  2. Then, in a new tab, sign in on shared-dev (dev.appname.companyname.com)
  3. Switch back to the localhost tab and click logout
  4. And finally, while still on the localhost tab, click login which will fail to login or provide an error message. GCP will record no SignInWithIdp calls and the getRedirectResult(auth) always produces a null result

Relevant Code:

// The config is the same for all environments
const config = {
  apiKey: AppConfig.VITE_API_KEY,
  authDomain: AppConfig.VITE_AUTH_DOMAIN,
  tenant: AppConfig.VITE_TENNANT,
};

  const provider = new OAuthProvider('microsoft.com').setCustomParameters({
    tenant: config.tenant,
  });

  signInWithRedirect(auth, provider);
}

(See below post for complete contextual narrative and code).

@TheNotary
Copy link
Author

TheNotary commented Feb 15, 2023

I've been working on this bug so long that I developed a deep sadness inside but also a better understanding of what's going wrong. We're using the Microsoft OAuthProvider to handle authentication of our company's Azure AD tenant.

I thought I fixed it a few hours after posting, but I see now that I was once again wrong; the app will eventually fall into a state where signing in no longer results in OAuth JWT data. It might have to do with our shared dev domain being tested along side our localhost which both share the same <project-id>.firebaseapp.com? But I think it's more likely that the problem lies with the Microsoft OAuthProvider. Update: It has something to do with tabs and the communication channel between the iframe and the main browser window. This probably isn't Microsoft specific and pertains to OAuthProviders and possibly more authentication code paths that I'm not currently investigating.

As an tangental observation, I've noticed that when I click logout of outlook.com, I'm eventually presented with a message stating "" (you can see this message when navigating to their OAuth signout "endpoint". That's always come to me as a peculiar note for an application to leave for their end-users. Also, it seems like logging back into outlook.com in the same browser works fine anyway, so I've been safely ignoring the message... that is until this fateful week...

So what I'm observing now is that, eventually the browser tab's state somehow becomes glitched! Clicking login and logout willy-nilly like a toddler that's just learned to click will function as expected, but eventually, it will stop working. getRedirectResult returns null as noted by almost a dozen bothans who upvoted this SO question at the time of this posting. Were they all using Microsoft.com as a provider? Who knows, they're all gone now. But we can at least honor their brave deeds by using the information they left with us to pursue a solution.

I can consistently glitch my local tab's state ('localhost:3000') by:

  1. Signing in at localhost,
  2. Then, in a new tab, signing in to shared-dev the same app
  3. Switching back to the localhost tab and clicking logout
  4. And finally while still on the localhost tab, clicking login which fails to login or provide an error message
const config = {
  apiKey: AppConfig.VITE_API_KEY,                        // Same for all environments
  authDomain: AppConfig.VITE_AUTH_DOMAIN,  // Same for all environments
  tenant: AppConfig.VITE_TENNANT,                      // Same for all environments
};

let app: FirebaseApp;
let auth: Auth;

async function initApp(): Promise<RedirectResult | null> {
  console.debug('INIT APP WAS CALLED');
  app  = await initializeApp(config);
  auth = await getAuth(app);

  // This doesn't help us with auth bugs... opening a new tab always seems to fix
  // the bug.  I think it's Microsoft's OAuth implementation that is broken
  // await auth.setPersistence(inMemoryPersistence);

  // Check if we're local
  // if (AppConfig.VITE_RPM_ENV === 'local-test')
  //   connectAuthEmulator(auth, 'http://localhost:9099');

  // This must happen immediately after getAuth occurs
  return await catchRedirectSignInMicrosoft();
}

async function signInMicrosoft(): Promise<void> {
  console.debug('signInMicrosoft called');
  const provider = new OAuthProvider('microsoft.com');
  provider.setCustomParameters({
    // prompt: 'login',
    tenant: config.tenant
  });
  console.debug('calling signInWithRedirect');
  signInWithRedirect(auth, provider).catch(error => console.log(error));
}

async function signOutMicrosoft(): Promise<void> {
  console.debug('signOutMicrosoft called');
  await auth.signOut();

  // console.log('redirecting to MS logout screen because their OAuth seems to be broken.');
  // window.location.href = 'https://login.windows.net/common/oauth2/logout';
}

async function catchRedirectSignInMicrosoft(): Promise<RedirectResult | null> {
  console.debug('catchRedirectSignInMicrosoft was called.');
  const result = await getRedirectResult(auth);

  if (result == null) {
    return null;
  }

  return {
    user:       result.user,
    credential: OAuthProvider.credentialFromResult(result)
  };
}


////////////////
// Init App 
///////////////

initApp().then(result => {
      if (result != null) {
        console.debug('catchRedirectSignInMicrosoft: User appears to be logged in with Firebase Auth.');
        const user = result.user;
        const credential = result.credential;

        let providerId = 'NO PROVIDER';
        if (user.providerData[0]?.providerId != null) providerId = user.providerData[0]?.providerId;
        console.log(`User logged in via ${providerId}`);

        userContext.fillUserInfoFromRedirect(user, credential);
        // Do app specific stuff following successful login
        return;
      }

      console.debug('catchRedirectSignInMicrosoft: resulted in a null return object... we probably have that horrible bug again...');

      if (checkIfWeAreInAPotentiallyStuckPendingState()) {
        console.log('loginStatus was pending, but catchRedirectSignInMicrosoft response was null.  Setting status to logged_out in case we\'re in a stuck state.');
        console.log('DEBUG: Skipping removal of pending state to analyze bug');
        userContext.setLoginStatus('logged_out');
        return;
      }

      const user = getFirebaseUser();
      if (checkIfAlreadyLoggedInViaSomePersistenceFromFirebase(user)) {
        // Check to make sure we're using the absolute latest token before connecting.
        user?.getIdToken()
          .then((token: string) => {
            userContext.setIdToken(token);
            // Do app specific stuff following successful login
          })
          .catch((err: any) => console.log(err));

        return;
      }

      if (checkIfOurLoginStatusIndicatesALoginEvenThoughFirebaseDisagrees()) {
        console.warn('catchRedirectSignInMicrosoft: returned a null Firebase user, and userContext.loginStatus was logged_in.  Logging out (possibly causing problems).');
        userContext.logout();
      }

    }).catch(err => console.log(err));

@TheNotary TheNotary changed the title Can't sign back in after sign out (caching?) OAuthProvider (Microsoft.com) and signInWithRedirect stop working following login from shared-dev Feb 15, 2023
@TheNotary
Copy link
Author

TheNotary commented Feb 17, 2023

It seems to be something going wrong with the response from the redirect iframe.

Working redirect flow:
working

Broken redirect flow:
broken

I guess maybe there are more answers in Request URL: https://firebaseapp.com/__/auth/iframe.js.

similar

I think we'd need someone familiar with more of the iframe/ indexed DB end of things to get much further unless someone is able to diagnose why the iframe would stop working correctly when another tab logs into a separate domain sharing the same API key (there something in the indexDB persistence that I thought might be worth following up on, but I've never worked with that API).

@TheNotary TheNotary changed the title OAuthProvider (Microsoft.com) and signInWithRedirect stop working following login from shared-dev OAuthProvider (Microsoft.com) and signInWithRedirect stop working following login from a second tab Feb 17, 2023
@TheNotary TheNotary changed the title OAuthProvider (Microsoft.com) and signInWithRedirect stop working following login from a second tab signInWithRedirect stops working following login from a second tab Feb 21, 2023
@TheNotary TheNotary changed the title signInWithRedirect stops working following login from a second tab signInWithRedirect stops working following login from a second tab on Macs Feb 21, 2023
@TheNotary TheNotary changed the title signInWithRedirect stops working following login from a second tab on Macs signInWithRedirect stops working following login from a second tab on Macs using Chrome Feb 21, 2023
@prameshj
Copy link
Contributor

This sounds a little like #6914 where an app initialize would clear the session storage. But it is different because 1) I see the sample code waits for app initialization before issuing a redirect login 2) The session storage shouldn't be shared across tabs.

Regarding:

iframe authEvent occured. type unknown

I assume you added this log in the event handler? Can you also log authEvent.error? The UNKNOWN type is thrown if there was no entry for "redirectEvent::" key in session storage OR if web storage was unvailable in the browser. I assume it is the former.

@TheNotary
Copy link
Author

TheNotary commented Feb 24, 2023

Sure, the console shows the authEvent including error details at the time of onAuthEvent being triggered:

Screen Shot 2023-02-23 at 6 14 36 PM

I'm surprised this bug isn't more expressed, I was beginning to think that it was a known bug related to browsers like Safari moving away from and you guys were holding while the situation developed. Following the article redirect best practices does fix the issue magically, so perhaps there aren't that many users depending on this code branch.

Btw, here's what I'm seeing around getting session storage items, but I don't have any testing/ visibility for what the iframe is doing. Is its code open source and available to review on GH? I've never debugged iframes before, but I'd imagine there's a lead in that thing, wherever it's trying to read session storage (or validate it's session storage is available).

Screen Shot 2023-02-23 at 6 42 00 PM

@prameshj
Copy link
Contributor

Thanks for the additional details. It confirms that the auth event is not present when the iframe tries to read it. It is strange that this only happens when you have more than one tab open. But the redirect best practices you linked are the best way to keep signInWithRedirect working in all scenarios, glad to hear that it works in this case.

The iframe code isn't open source unfortunately. It does a session storage read of a specific key and returns no-auth-event if it is empty.

I am closing this since it works when the auth helper code is on the same domain, by following best practices.

@TheNotary
Copy link
Author

Thanks for these extra details and for your help supporting Firebase Auth. I'm not entirely convinced this is a closed issue however because it cost me a lot of effort sorting out what was going wrong and ultimately found that neither myself nor any available maintainers were able to fix the bug despite its remarkable reproducibility on multiple engineer's MacBook Pros.

As an engineer adopting Firebase Auth, I would appreciate the redirect best practices article using much stronger, clarifying language to indicate that Using signInWithRedirect with a cross-origin authDomain is only intended as an easy way to get started with firebase Auth in development, but it is not a production ready solution due to the complexities involved in maintaining the OAuth authentication scheme across multiple operating systems varied by multiple browsers each with their own unique implementations constraining the niche of iframe development. This will help save users from investing time bug tracking a code path that is not fully open source.

Alternatively, exposing the non-minified version of the OAuth helper scripts seems like a good idea too because then open source development and bug fixes would become a possibility bringing the necessary strength to the capability.

@prameshj
Copy link
Contributor

Fair point.. I reopened this to track changing the best-practices doc to call out the requirements as you mentioned.

@prameshj prameshj reopened this Feb 27, 2023
@prameshj prameshj self-assigned this Feb 27, 2023
@prameshj
Copy link
Contributor

We did update the docs in https://firebase.google.com/docs/auth/web/redirect-best-practices to mention "You must follow one of the options listed here for signInWithRedirect() to function as intended in production environments, across all browsers."

Please reopen if you have more suggestions here.

@firebase firebase locked and limited conversation to collaborators Apr 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants