From ade3b54a37863c27eb886a724ab30875a9c2a2ca Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 15 Apr 2022 10:55:28 -0700 Subject: [PATCH 1/4] Add integration tests for auth middleware --- .../test/integration/flows/anonymous.test.ts | 5 + .../integration/flows/custom.local.test.ts | 5 + .../auth/test/integration/flows/email.test.ts | 5 + .../test/integration/flows/idp.local.test.ts | 8 + .../flows/middleware_test_generator.ts | 161 ++++++++++++++++++ .../test/integration/flows/oob.local.test.ts | 9 + .../auth/test/integration/flows/phone.test.ts | 6 + 7 files changed, 199 insertions(+) create mode 100644 packages/auth/test/integration/flows/middleware_test_generator.ts diff --git a/packages/auth/test/integration/flows/anonymous.test.ts b/packages/auth/test/integration/flows/anonymous.test.ts index 3e085dbcd37..eb260dba365 100644 --- a/packages/auth/test/integration/flows/anonymous.test.ts +++ b/packages/auth/test/integration/flows/anonymous.test.ts @@ -37,6 +37,7 @@ import { getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -128,4 +129,8 @@ describe('Integration test: anonymous auth', () => { ); }); }); + + generateMiddlewareTests(() => auth, () => { + return signInAnonymously(auth); + }); }); diff --git a/packages/auth/test/integration/flows/custom.local.test.ts b/packages/auth/test/integration/flows/custom.local.test.ts index c9b1f0d1d42..a35fa40d78a 100644 --- a/packages/auth/test/integration/flows/custom.local.test.ts +++ b/packages/auth/test/integration/flows/custom.local.test.ts @@ -39,6 +39,7 @@ import { getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -225,4 +226,8 @@ describe('Integration test: custom auth', () => { ); }); }); + + generateMiddlewareTests(() => auth, () => { + return signInWithCustomToken(auth, customToken); + }); }); diff --git a/packages/auth/test/integration/flows/email.test.ts b/packages/auth/test/integration/flows/email.test.ts index 12a27e84358..ced1e2ef7e4 100644 --- a/packages/auth/test/integration/flows/email.test.ts +++ b/packages/auth/test/integration/flows/email.test.ts @@ -38,6 +38,7 @@ import { getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -168,5 +169,9 @@ describe('Integration test: email/password auth', () => { ); expect(userA.uid).to.eq(userB.uid); }); + + generateMiddlewareTests(() => auth, () => { + return signInWithEmailAndPassword(auth, email, 'password'); + }); }); }); diff --git a/packages/auth/test/integration/flows/idp.local.test.ts b/packages/auth/test/integration/flows/idp.local.test.ts index 4e42ac9d405..07fa788cd77 100644 --- a/packages/auth/test/integration/flows/idp.local.test.ts +++ b/packages/auth/test/integration/flows/idp.local.test.ts @@ -41,6 +41,7 @@ import { getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -285,4 +286,11 @@ describe('Integration test: headless IdP', () => { 'github.com' ]); }); + + generateMiddlewareTests(() => auth, () => { + return signInWithCredential( + auth, + GoogleAuthProvider.credential(oauthIdToken) + ); + }); }); diff --git a/packages/auth/test/integration/flows/middleware_test_generator.ts b/packages/auth/test/integration/flows/middleware_test_generator.ts new file mode 100644 index 00000000000..06c801308e7 --- /dev/null +++ b/packages/auth/test/integration/flows/middleware_test_generator.ts @@ -0,0 +1,161 @@ +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import {Auth, createUserWithEmailAndPassword, User} from '@firebase/auth'; +import { randomEmail } from '../../helpers/integration/helpers'; + +use(chaiAsPromised); +use(sinonChai); + +export function generateMiddlewareTests(authGetter: () => Auth, signIn: () => Promise): void { + context('middleware', () => { + let auth: Auth; + let unsubscribes: Array<() => void>; + + beforeEach(() => { + auth = authGetter(); + unsubscribes = []; + }); + + afterEach(() => { + for (const u of unsubscribes) { + u(); + } + }); + + /** + * Helper function for adding beforeAuthStateChanged that will + * automatically unsubscribe after every test (since some tests may + * perform cleanup after that would be affected by the middleware) + */ + function beforeAuthStateChanged(callback: (user: User | null) => void | Promise): void { + unsubscribes.push(auth.beforeAuthStateChanged(callback)); + } + + it('can prevent user sign in', async () => { + beforeAuthStateChanged(() => { + throw new Error('stop sign in'); + }); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.be.null; + }); + + it('keeps previously-logged in user if blocked', async () => { + // Use a random email/password sign in for the base user + const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); + + beforeAuthStateChanged(() => { + throw new Error('stop sign in'); + }); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.eq(baseUser); + }); + + it('can allow sign in', async () => { + beforeAuthStateChanged(() => { + // Pass + }); + + await expect(signIn()).not.to.be.rejected; + expect(auth.currentUser).not.to.be.null; + }); + + it('overrides previous user if allowed', async () => { + // Use a random email/password sign in for the base user + const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); + + beforeAuthStateChanged(() => { + // Pass + }); + + await expect(signIn()).not.to.be.rejected; + expect(auth.currentUser).not.to.eq(baseUser); + }); + + it('will reject if one callback fails', async () => { + // Also check that the function is called multiple + // times + const spy = sinon.spy(); + + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + beforeAuthStateChanged(() => { + throw new Error('stop sign in'); + }); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.be.null; + expect(spy).to.have.been.calledThrice; + }); + + it('keeps previously-logged in user if one rejects', async () => { + // Use a random email/password sign in for the base user + const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); + + // Also check that the function is called multiple + // times + const spy = sinon.spy(); + + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + beforeAuthStateChanged(() => { + throw new Error('stop sign in'); + }); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.eq(baseUser); + expect(spy).to.have.been.calledThrice; + }); + + it('allows sign in with multiple callbacks all pass', async () => { + // Use a random email/password sign in for the base user + const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); + + // Also check that the function is called multiple + // times + const spy = sinon.spy(); + + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + beforeAuthStateChanged(spy); + + await expect(signIn()).not.to.be.rejected; + expect(auth.currentUser).not.to.eq(baseUser); + expect(spy).to.have.been.calledThrice; + }); + + it('does not call subsequent callbacks after rejection', async () => { + const firstSpy = sinon.spy(); + const secondSpy = sinon.spy(); + + beforeAuthStateChanged(firstSpy); + beforeAuthStateChanged(() => { + throw new Error('stop sign in'); + }); + beforeAuthStateChanged(secondSpy); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(firstSpy).to.have.been.calledOnce; + expect(secondSpy).not.to.have.been.called; + }); + + it('can prevent sign-out', async () => { + await signIn(); + const user = auth.currentUser; + + beforeAuthStateChanged(() => { + throw new Error('block sign out'); + }); + + await expect(auth.signOut()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.eq(user); + }); + }); +} \ No newline at end of file diff --git a/packages/auth/test/integration/flows/oob.local.test.ts b/packages/auth/test/integration/flows/oob.local.test.ts index 059313a4ce7..49b6f45280f 100644 --- a/packages/auth/test/integration/flows/oob.local.test.ts +++ b/packages/auth/test/integration/flows/oob.local.test.ts @@ -53,6 +53,7 @@ import { getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -267,6 +268,14 @@ describe('Integration test: oob codes', () => { signInWithEmailLink(auth, email, otherSession.oobLink) ).to.be.rejectedWith(FirebaseError, 'auth/invalid-email'); }); + + generateMiddlewareTests(() => auth, () => { + return signInWithEmailLink( + auth, + email, + oobSession.oobLink + ); + }); }); it('can be used to verify email', async () => { diff --git a/packages/auth/test/integration/flows/phone.test.ts b/packages/auth/test/integration/flows/phone.test.ts index 54ed016c81a..2711292d046 100644 --- a/packages/auth/test/integration/flows/phone.test.ts +++ b/packages/auth/test/integration/flows/phone.test.ts @@ -42,6 +42,7 @@ import { getTestInstance } from '../../helpers/integration/helpers'; import { getPhoneVerificationCodes } from '../../helpers/integration/emulator_rest_helpers'; +import { generateMiddlewareTests } from './middleware_test_generator'; use(chaiAsPromised); @@ -306,4 +307,9 @@ describe('Integration test: phone auth', () => { expect(errorUserCred.user.uid).to.eq(signUpCred.user.uid); }); }); + + generateMiddlewareTests(() => auth, async () => { + const cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier); + await cr.confirm(await code(cr, PHONE_A.code)); + }); }); From 34d3fcc62d93d9847cacfd2bdcb4b88a322f41d6 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 15 Apr 2022 11:56:34 -0700 Subject: [PATCH 2/4] Webdriver tests --- .../flows/middleware_test_generator.ts | 18 +++++++++ .../integration/webdriver/persistence.test.ts | 39 ++++++++++++++++++- .../test/integration/webdriver/popup.test.ts | 29 ++++++++++++++ .../integration/webdriver/redirect.test.ts | 23 +++++++++++ .../integration/webdriver/static/index.js | 2 + .../webdriver/static/middleware.js | 16 ++++++++ .../integration/webdriver/util/functions.ts | 5 +++ 7 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 packages/auth/test/integration/webdriver/static/middleware.js diff --git a/packages/auth/test/integration/flows/middleware_test_generator.ts b/packages/auth/test/integration/flows/middleware_test_generator.ts index 06c801308e7..e9310e9fe99 100644 --- a/packages/auth/test/integration/flows/middleware_test_generator.ts +++ b/packages/auth/test/integration/flows/middleware_test_generator.ts @@ -44,6 +44,15 @@ export function generateMiddlewareTests(authGetter: () => Auth, signIn: () => Pr expect(auth.currentUser).to.be.null; }); + it('can prevent user sign in as a promise', async () => { + beforeAuthStateChanged(() => { + return Promise.reject('stop sign in'); + }); + + await expect(signIn()).to.be.rejectedWith('auth/login-blocked'); + expect(auth.currentUser).to.be.null; + }); + it('keeps previously-logged in user if blocked', async () => { // Use a random email/password sign in for the base user const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); @@ -65,6 +74,15 @@ export function generateMiddlewareTests(authGetter: () => Auth, signIn: () => Pr expect(auth.currentUser).not.to.be.null; }); + it('can allow sign in as a promise', async () => { + beforeAuthStateChanged(() => { + return Promise.resolve(); + }); + + await expect(signIn()).not.to.be.rejected; + expect(auth.currentUser).not.to.be.null; + }); + it('overrides previous user if allowed', async () => { // Use a random email/password sign in for the base user const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password'); diff --git a/packages/auth/test/integration/webdriver/persistence.test.ts b/packages/auth/test/integration/webdriver/persistence.test.ts index bc76609a0fa..1de9e0c282d 100644 --- a/packages/auth/test/integration/webdriver/persistence.test.ts +++ b/packages/auth/test/integration/webdriver/persistence.test.ts @@ -17,18 +17,22 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { UserCredential } from '@firebase/auth'; -import { expect } from 'chai'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import { createAnonAccount } from '../../helpers/integration/emulator_rest_helpers'; import { API_KEY } from '../../helpers/integration/settings'; import { START_FUNCTION } from './util/auth_driver'; import { AnonFunction, CoreFunction, + MiddlewareFunction, PersistenceFunction } from './util/functions'; import { JsLoadCondition } from './util/js_load_condition'; import { browserDescribe } from './util/test_runner'; +use(chaiAsPromised); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async function testPersistedUser() { const account = await createAnonAccount(); @@ -458,6 +462,39 @@ browserDescribe('WebDriver persistence test', (driver, browser) => { expect(await driver.getUserSnapshot()).to.contain({ uid: uid2 }); }); + it('middleware does not block tab sync', async () => { + if (driver.isCompatLayer()) { + // Compat layer is skipped because it doesn't support middleware + console.warn('Skipping middleware tabs in compat test'); + return; + } + + // Blocking middleware in main page + await driver.call(MiddlewareFunction.ATTACH_BLOCKING_MIDDLEWARE); + + // Check that it blocks basic sign in + await expect(driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + )).to.be.rejectedWith('auth/login-blocked'); + const userInPopup = await driver.getUserSnapshot(); + expect(userInPopup).to.be.null; + + // Now sign in in new page + await driver.webDriver.executeScript('window.open(".");'); + await driver.selectPopupWindow(); + await driver.webDriver.wait(new JsLoadCondition(START_FUNCTION)); + await driver.injectConfigAndInitAuth(); + await driver.waitForAuthInit(); + const cred: UserCredential = await driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + ); + + // And make sure it was updated in main window + await driver.selectMainWindow({ noWait: true }); + await driver.pause(700); + expect((await driver.getUserSnapshot()).uid).to.eq(cred.user.uid); + }); + it('sync current user across windows with localStorage', async () => { await driver.webDriver.navigate().refresh(); // Simulate browsers that do not support indexedDB. diff --git a/packages/auth/test/integration/webdriver/popup.test.ts b/packages/auth/test/integration/webdriver/popup.test.ts index e90823cb512..3d46d171ee6 100644 --- a/packages/auth/test/integration/webdriver/popup.test.ts +++ b/packages/auth/test/integration/webdriver/popup.test.ts @@ -30,6 +30,7 @@ import { AnonFunction, CoreFunction, EmailFunction, + MiddlewareFunction, PopupFunction } from './util/functions'; @@ -63,6 +64,34 @@ browserDescribe('Popup IdP tests', driver => { expect(result.user).to.eql(currentUser); }); + it('is blocked by auth middleware', async () => { + if (driver.isCompatLayer()) { + // Compat layer doesn't support middleware yet + return; + } + + await driver.call(MiddlewareFunction.ATTACH_BLOCKING_MIDDLEWARE); + await driver.callNoWait(PopupFunction.IDP_POPUP); + await driver.selectPopupWindow(); + const widget = new IdPPage(driver.webDriver); + + // We're now on the widget page; wait for load + await widget.pageLoad(); + await widget.clickAddAccount(); + await widget.fillEmail('bob@bob.test'); + await widget.fillDisplayName('Bob Test'); + await widget.fillScreenName('bob.test'); + await widget.fillProfilePhoto('http://bob.test/bob.png'); + await widget.clickSignIn(); + + await driver.selectMainWindow(); + await expect(driver.call( + PopupFunction.POPUP_RESULT + )).to.be.rejectedWith('auth/login-blocked'); + const currentUser = await driver.getUserSnapshot(); + expect(currentUser).to.be.null; + }); + it('can link with another account account', async () => { // First, sign in anonymously const { user: anonUser }: UserCredential = await driver.call( diff --git a/packages/auth/test/integration/webdriver/redirect.test.ts b/packages/auth/test/integration/webdriver/redirect.test.ts index 2bc17411637..ce7841f2090 100644 --- a/packages/auth/test/integration/webdriver/redirect.test.ts +++ b/packages/auth/test/integration/webdriver/redirect.test.ts @@ -30,8 +30,11 @@ import { AnonFunction, CoreFunction, EmailFunction, + MiddlewareFunction, RedirectFunction } from './util/functions'; +import { JsLoadCondition } from './util/js_load_condition'; +import { START_FUNCTION } from './util/auth_driver'; use(chaiAsPromised); @@ -70,6 +73,26 @@ browserDescribe('WebDriver redirect IdP test', driver => { expect(await driver.call(RedirectFunction.REDIRECT_RESULT)).to.be.null; }); + // Redirect works with middleware for now + xit('is blocked by middleware', async () => { + await driver.callNoWait(RedirectFunction.IDP_REDIRECT); + const widget = new IdPPage(driver.webDriver); + + // We're now on the widget page; wait for load + await widget.pageLoad(); + await widget.clickAddAccount(); + await widget.fillEmail('bob@bob.test'); + await widget.fillDisplayName('Bob Test'); + await widget.fillScreenName('bob.test'); + await widget.fillProfilePhoto('http://bob.test/bob.png'); + await widget.clickSignIn(); + await driver.webDriver.wait(new JsLoadCondition(START_FUNCTION)); + await driver.call(MiddlewareFunction.ATTACH_BLOCKING_MIDDLEWARE_ON_START); + + await driver.reinitOnRedirect(); + expect(await driver.getUserSnapshot()).to.be.null; + }); + it('can link with another account account', async () => { // First, sign in anonymously const { user: anonUser }: UserCredential = await driver.call( diff --git a/packages/auth/test/integration/webdriver/static/index.js b/packages/auth/test/integration/webdriver/static/index.js index 86471a6bd31..0dff994fcc1 100644 --- a/packages/auth/test/integration/webdriver/static/index.js +++ b/packages/auth/test/integration/webdriver/static/index.js @@ -21,6 +21,7 @@ import * as core from './core'; import * as popup from './popup'; import * as email from './email'; import * as persistence from './persistence'; +import * as middleware from './middleware'; import { initializeApp } from '@firebase/app'; import { getAuth, connectAuthEmulator } from '@firebase/auth'; @@ -30,6 +31,7 @@ window.redirect = redirect; window.popup = popup; window.email = email; window.persistence = persistence; +window.middleware = middleware; window.auth = null; window.legacyAuth = null; diff --git a/packages/auth/test/integration/webdriver/static/middleware.js b/packages/auth/test/integration/webdriver/static/middleware.js new file mode 100644 index 00000000000..039fc375e15 --- /dev/null +++ b/packages/auth/test/integration/webdriver/static/middleware.js @@ -0,0 +1,16 @@ +export async function attachBlockingMiddleware() { + auth.beforeAuthStateChanged(() => { + throw new Error('block state change'); + }); +} + +export async function attachBlockingMiddlewareOnStart() { + // Attaches the blocking middleware _immediately_ after auth is initialized, + // allowing us to test redirect operations. + const oldStartAuth = window.startAuth; + + window.startAuth = async () => { + oldStartAuth(); + await attachBlockingMiddleware(); + } +} \ No newline at end of file diff --git a/packages/auth/test/integration/webdriver/util/functions.ts b/packages/auth/test/integration/webdriver/util/functions.ts index 6350f59e9f9..42c8efd8b35 100644 --- a/packages/auth/test/integration/webdriver/util/functions.ts +++ b/packages/auth/test/integration/webdriver/util/functions.ts @@ -77,6 +77,11 @@ export enum PersistenceFunction { SET_PERSISTENCE_LOCAL_STORAGE = 'persistence.setPersistenceLocalStorage' } +export enum MiddlewareFunction { + ATTACH_BLOCKING_MIDDLEWARE = 'middleware.attachBlockingMiddleware', + ATTACH_BLOCKING_MIDDLEWARE_ON_START = 'middleware.attachBlockingMiddlewareOnStart', +} + /** Available firebase UI functions (only for compat tests) */ export enum UiFunction { LOAD = 'ui.loadUiCode', From c8b3ebafba28f3becb8e1c7edea306dcd6771109 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 15 Apr 2022 12:02:24 -0700 Subject: [PATCH 3/4] Lint --- packages/auth/test/integration/webdriver/redirect.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/auth/test/integration/webdriver/redirect.test.ts b/packages/auth/test/integration/webdriver/redirect.test.ts index ce7841f2090..137a90106ca 100644 --- a/packages/auth/test/integration/webdriver/redirect.test.ts +++ b/packages/auth/test/integration/webdriver/redirect.test.ts @@ -38,6 +38,8 @@ import { START_FUNCTION } from './util/auth_driver'; use(chaiAsPromised); +declare const xit: typeof it; + browserDescribe('WebDriver redirect IdP test', driver => { beforeEach(async () => { await driver.pause(200); // Race condition on auth init From 7f8c8b05a04fbea9561cf6a4484374d0e82c3c7c Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 15 Apr 2022 12:02:49 -0700 Subject: [PATCH 4/4] Formatting --- .../flows/middleware_test_generator.ts | 17 +++++++++++++++++ .../integration/webdriver/static/middleware.js | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/auth/test/integration/flows/middleware_test_generator.ts b/packages/auth/test/integration/flows/middleware_test_generator.ts index e9310e9fe99..d4c1324f3de 100644 --- a/packages/auth/test/integration/flows/middleware_test_generator.ts +++ b/packages/auth/test/integration/flows/middleware_test_generator.ts @@ -1,3 +1,20 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; diff --git a/packages/auth/test/integration/webdriver/static/middleware.js b/packages/auth/test/integration/webdriver/static/middleware.js index 039fc375e15..a4a2d90c801 100644 --- a/packages/auth/test/integration/webdriver/static/middleware.js +++ b/packages/auth/test/integration/webdriver/static/middleware.js @@ -1,3 +1,20 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export async function attachBlockingMiddleware() { auth.beforeAuthStateChanged(() => { throw new Error('block state change');