Repository navigation
Conversation
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.
There was a problem hiding this comment.
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.
| web.window.localStorage.setItem( | ||
| authEmulatorOriginStorageKey(appName), | ||
| origin, | ||
| ); |
There was a problem hiding this comment.
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
}| if (authDelegate.emulatorOrigin == emulatorOrigin) { | ||
| _rememberAuthEmulatorOrigin(firebaseApp.name, emulatorOrigin!); | ||
| } |
There was a problem hiding this comment.
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.
| if (authDelegate.emulatorOrigin == emulatorOrigin) { | |
| _rememberAuthEmulatorOrigin(firebaseApp.name, emulatorOrigin!); | |
| } | |
| final origin = emulatorOrigin; | |
| if (origin != null && authDelegate.emulatorOrigin == origin) { | |
| _rememberAuthEmulatorOrigin(firebaseApp.name, origin); | |
| } |
References
- 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); |
There was a problem hiding this comment.
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.
| expect(await restored.currentUser!.getIdToken(true), isNotEmpty); | |
| expect(await restored.currentUser?.getIdToken(true), isNotEmpty); |
References
- 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.
| // 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' |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Requesting changes for the localStorage persistence. The rest is non-blocking.
| requestedOrigin: origin, | ||
| connectedOrigin: delegate.emulatorOrigin, | ||
| )) { | ||
| _rememberAuthEmulatorOrigin(delegate.app.name, origin); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
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 calluseAuthEmulator, and the plugin swallowed the failed reconnect (auth/emulator-config-failed). A reload of the same tab was already fine, becausesessionStorageis still there.Debug builds now write the origin to
localStorageafter 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 leftoversessionStoragevalue is still read whenlocalStorageis empty. The debug log says to clear this site's storage to go back to production. Release builds do not write the key.When
connectAuthEmulatoris 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,useAuthEmulatorthrowsemulator-config-failedwhen 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.yamland changelogs is not required.///).melos run analyze) does not report any problems on my PR.Breaking Change
Does your PR require plugin users to manually update their apps to accommodate your change?
Test plan
flutter test test/auth_emulator_test.dartinpackages/firebase_auth/firebase_auth_web(29 passed).dart analyze --fatal-infosinfirebase_auth_web.flutter drive --target=./integration_test/auth_emulator_web_e2e_test.dart --driver=./test_driver/integration_test.dart -d web-serverwith ChromeDriver and the Auth emulator. Four tests passed: the persisted-user case, a production sign-in that then throwsemulator-config-failed, and a late connect to a different origin that does not throw.melos run analyzefor the whole repo was not run.firebase_authweb e2e suite was not run. The new cases are part ofintegration_test/e2e_test.dart.