G
YouTube TikTok Games
Skip to content

fix(auth): keep the web Auth emulator across a browser restart - #18926

Open
SelaseKay wants to merge 5 commits into
mainfrom
fix-18888-auth-emulator-persisted-user
Open

SelaseKay wants to merge 5 commits into
mainfrom
fix-18888-auth-emulator-persisted-user

Conversation

@SelaseKay

@SelaseKay SelaseKay commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

On web, a debug build remembered the Auth emulator origin in sessionStorage. That value dies when the browser process exits. The signed-in user does not: it stays in IndexedDB. The next launch restored that user against production before the app could call useAuthEmulator, and the plugin swallowed the failed reconnect (auth/emulator-config-failed). A reload of the same tab was already fine, because sessionStorage is still there.

Debug builds now write the origin to localStorage after a successful connect, and also when a connect is rejected because this Auth instance is already locked on a different origin. The next full page load applies that origin before Auth restores the user. A leftover sessionStorage value is still read when localStorage is empty. The debug log says to clear this site's storage to go back to production. Release builds do not write the key.

When connectAuthEmulator is refused, the log names the backend this instance is actually on. The "already configured, kept across hot reload" line is used only when this instance is already on the requested origin. Hot restart keeps the JS Auth instance, so a stored origin applies after a full page reload. In debug, useAuthEmulator throws emulator-config-failed when this instance is still on production, after the origin has been stored.

Related Issues

Fixes #18888

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]).
This will ensure a smooth and quick review process. Updating the pubspec.yaml and changelogs is not required.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on my PR.
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Test plan

  • flutter test test/auth_emulator_test.dart in packages/firebase_auth/firebase_auth_web (29 passed).
  • dart analyze --fatal-infos in firebase_auth_web.
  • flutter drive --target=./integration_test/auth_emulator_web_e2e_test.dart --driver=./test_driver/integration_test.dart -d web-server with ChromeDriver and the Auth emulator. Four tests passed: the persisted-user case, a production sign-in that then throws emulator-config-failed, and a late connect to a different origin that does not throw.
  • melos run analyze for the whole repo was not run.
  • The full firebase_auth web e2e suite was not run. The new cases are part of integration_test/e2e_test.dart.

sessionStorage does not survive the browser process exiting, so a persisted user was restored against production. Debug builds now remember the emulator origin in localStorage and apply it before that restore.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Firebase Auth Web plugin to persist the configured Auth emulator origin across page reloads and browser sessions using localStorage (with a fallback to sessionStorage), and adds corresponding integration tests. The review feedback highlights potential runtime crashes (SecurityError) when accessing browser storage in restricted environments (such as iframes or private browsing), recommending wrapping these accesses in try-catch blocks and limiting them to kDebugMode. Additionally, the feedback points out violations of the repository's style guide regarding the use of the non-null assertion operator (!), suggesting safer null-handling alternatives.

Comment on lines +633 to +636
web.window.localStorage.setItem(
authEmulatorOriginStorageKey(appName),
origin,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Accessing localStorage.setItem can throw a SecurityError (DOMException) if storage is disabled or restricted in the browser. We should wrap this call in a try-catch block to prevent crashes during debug/development.

  try {
    web.window.localStorage.setItem(
      authEmulatorOriginStorageKey(appName),
      origin,
    );
  } catch (_) {
    // Storage may be disabled or restricted
  }

Comment on lines +83 to +85
if (authDelegate.emulatorOrigin == emulatorOrigin) {
_rememberAuthEmulatorOrigin(firebaseApp.name, emulatorOrigin!);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

According to the repository style guide (Rule 23: 'Null Safety: NO ! operator. Use ? and flow analysis'), we should avoid using the ! operator.\n\nWe can avoid the ! operator here by assigning emulatorOrigin to a local non-nullable variable after checking it for null.

Suggested change
if (authDelegate.emulatorOrigin == emulatorOrigin) {
_rememberAuthEmulatorOrigin(firebaseApp.name, emulatorOrigin!);
}
final origin = emulatorOrigin;
if (origin != null && authDelegate.emulatorOrigin == origin) {
_rememberAuthEmulatorOrigin(firebaseApp.name, origin);
}
References
  1. Null Safety: NO ! operator. Use ? and flow analysis (e.g. if (x != null)). (link)

);
final restored = FirebaseAuth.instanceFor(app: app);
expect(restored.currentUser?.email, email);
expect(await restored.currentUser!.getIdToken(true), isNotEmpty);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

According to the repository style guide (Rule 23: 'Null Safety: NO ! operator. Use ? and flow analysis'), we should avoid using the ! operator.\n\nWe can use the conditional member access operator ?. instead.

Suggested change
expect(await restored.currentUser!.getIdToken(true), isNotEmpty);
expect(await restored.currentUser?.getIdToken(true), isNotEmpty);
References
  1. Null Safety: NO ! operator. Use ? and flow analysis (e.g. if (x != null)). (link)

Release startup was reading storage before the debug check, so a page that blocks storage could fail Firebase init. Debug writes now swallow that same failure.
@SelaseKay
SelaseKay marked this pull request as ready for review November 10, 2026 08:49
// ignore: avoid_print
print(
'Auth emulator is already configured at $emulatorOrigin for ${firebaseApp.name} and kept across hot reload.\nTo switch back to production, restart your app with the emulator turned off.',
'Auth emulator is already configured at $emulatorOrigin for ${firebaseApp.name} and kept across hot reload.\n'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With JS SDK 13.2, connectAuthEmulator returns early when the config matches. So this branch only runs when this instance is on production or on a different origin. That means the "already configured … kept across hot reload" message is wrong every time it prints, and the authDelegate.emulatorOrigin == emulatorOrigin check above never passes.

Before this PR, this branch was hard to reach. Now it's easy: a failed useAuthEmulator stores the origin, the developer presses R (hot restart keeps the JS Auth instance), and the console says the emulator is configured while the instance is still on production.

Could this print the current message only when the origins match? Otherwise it could say which origin the instance is actually on (or production) and that a full page reload is needed.

if (kDebugMode) {
// ignore: avoid_print
print(
'Auth emulator at $origin for ${delegate.app.name} will apply on the next launch.\n'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When delegate.emulatorOrigin is null here, the app asked for the emulator but stays on production for the rest of the session. That's the "it kept talking to prod" part of #18888. Now that shouldSkipConnectAuthEmulator handles the hot restart case, this error is only swallowed when the instance really isn't on the requested emulator. Would it make sense to throw in debug here? A reload fixes it now that the origin is stored. At the least, a louder warning would help.

Also, hot restart keeps the JS Auth instance, so "will apply on the next launch" should probably say "after a full page reload".

JS SDK 13.2 only refuses connectAuthEmulator when this instance is not already on that origin. Debug builds now name the backend they are still on, and throw if that backend is production after the origin is stored.
The auth package analyzer treats prefer_const_declarations as a failure.
Keep the JS error check from #18923, and still store the emulator origin, throwing in debug when this instance is on production.

@demolaf demolaf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the localStorage persistence. The rest is non-blocking.

requestedOrigin: origin,
connectedOrigin: delegate.emulatorOrigin,
)) {
_rememberAuthEmulatorOrigin(delegate.app.name, origin);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write the origin to sessionStorage here, not localStorage. A refused connect never reached the emulator, so persisting it across browser restarts pins later debug launches to an emulator the app may no longer ask for. The new debug throw plus one reload already covers #18888.

required bool isDebugMode,
required String? connectedOrigin,
}) {
return isDebugMode && connectedOrigin == null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also throw when the instance is on a different emulator? Right now useAuthEmulator resolves while staying on the old origin, which is the same silent mismatch this PR fixes for production.

required String? localOrigin,
required String? sessionOrigin,
}) {
return localOrigin ?? sessionOrigin;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider dropping the sessionStorage fallback. Nothing writes it any more, and a leftover value gets copied back into localStorage after someone clears it.

required String requestedOrigin,
required String? connectedOrigin,
}) {
if (connectedOrigin == requestedOrigin) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth removing this matching-origin branch and the check at firebase_auth_web.dart:87? JS SDK 13.2 returns early on a match, so neither caller can reach it.

)) {
_rememberAuthEmulatorOrigin(delegate.app.name, origin);
}
if (shouldThrowDebugEmulatorConnectFailure(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we say in the PR description that useAuthEmulator now throws emulator-config-failed in debug on web?

final auth = FirebaseAuth.instanceFor(app: app);
try {
await auth
.signInWithEmailAndPassword(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this lock the instance without a real sign-in against production? It depends on network access and adds up to 20s when that's missing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FirebaseAuth]: Reopen, Again (2): Emulator in development doesn't work after logged in / Loading use from session storage..

3 participants