diff --git a/.changeset/gold-geckos-carry.md b/.changeset/gold-geckos-carry.md new file mode 100644 index 00000000000..448b2523d7c --- /dev/null +++ b/.changeset/gold-geckos-carry.md @@ -0,0 +1,6 @@ +--- +"@firebase/auth-compat": patch +"@firebase/auth": patch +--- + +Fix errors in compatibility layer when cookies are fully disabled in Chrome diff --git a/packages/auth-compat/src/auth.test.ts b/packages/auth-compat/src/auth.test.ts index 4ccae935d78..0299170827f 100644 --- a/packages/auth-compat/src/auth.test.ts +++ b/packages/auth-compat/src/auth.test.ts @@ -23,9 +23,14 @@ import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; import { Auth } from './auth'; import { CompatPopupRedirectResolver } from './popup_redirect'; +import * as platform from './platform'; use(sinonChai); +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + // For the most part, the auth methods just call straight through. Some parts // of the auth compat layer are more complicated: these tests cover those describe('auth compat', () => { @@ -45,7 +50,7 @@ describe('auth compat', () => { }); afterEach(() => { - sinon.restore; + sinon.restore(); }); it('saves the persistence into session storage if available', async () => { @@ -75,6 +80,40 @@ describe('auth compat', () => { } }); + it('does not save persistence if property throws DOMException', async () => { + if (typeof self !== 'undefined') { + sinon.stub(platform, '_getSelfWindow').returns({ + get sessionStorage(): Storage { + throw new DOMException('Nope!'); + } + } as unknown as Window); + const setItemSpy = sinon.spy(sessionStorage, 'setItem'); + sinon.stub(underlyingAuth, '_getPersistence').returns('TEST'); + sinon + .stub(underlyingAuth, '_initializationPromise') + .value(Promise.resolve()); + sinon.stub( + exp._getInstance( + CompatPopupRedirectResolver + ), + '_openRedirect' + ); + providerStub.isInitialized.returns(true); + providerStub.getImmediate.returns(underlyingAuth); + const authCompat = new Auth( + app, + providerStub as unknown as Provider<'auth'> + ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await authCompat.signInWithRedirect(new exp.GoogleAuthProvider()); + await delay(50); + expect(setItemSpy).not.to.have.been.calledWith( + 'firebase:persistence:api-key:undefined', + 'TEST' + ); + } + }); + it('pulls the persistence and sets as the main persitsence if set', () => { if (typeof self !== 'undefined') { sessionStorage.setItem( @@ -98,5 +137,35 @@ describe('auth compat', () => { }); } }); + + it('does not die if sessionStorage errors', async () => { + if (typeof self !== 'undefined') { + sinon.stub(platform, '_getSelfWindow').returns({ + get sessionStorage(): Storage { + throw new DOMException('Nope!'); + } + } as unknown as Window); + sessionStorage.setItem( + 'firebase:persistence:api-key:undefined', + 'none' + ); + providerStub.isInitialized.returns(false); + providerStub.initialize.returns(underlyingAuth); + new Auth(app, providerStub as unknown as Provider<'auth'>); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await delay(50); + expect(providerStub.initialize).to.have.been.calledWith({ + options: { + popupRedirectResolver: CompatPopupRedirectResolver, + persistence: [ + exp.indexedDBLocalPersistence, + exp.browserLocalPersistence, + exp.browserSessionPersistence, + exp.inMemoryPersistence + ] + } + }); + } + }); }); }); diff --git a/packages/auth-compat/src/persistence.ts b/packages/auth-compat/src/persistence.ts index b8e945da0df..c3f046828d7 100644 --- a/packages/auth-compat/src/persistence.ts +++ b/packages/auth-compat/src/persistence.ts @@ -17,7 +17,7 @@ import * as exp from '@firebase/auth/internal'; import { isIndexedDBAvailable, isNode, isReactNative } from '@firebase/util'; -import { _isWebStorageSupported, _isWorker } from './platform'; +import { _getSelfWindow, _isWebStorageSupported, _isWorker } from './platform'; export const Persistence = { LOCAL: 'local', @@ -84,15 +84,14 @@ export async function _savePersistenceForRedirect( auth: exp.AuthInternal ): Promise { await auth._initializationPromise; - - const win = getSelfWindow(); + const session = getSessionStorageIfAvailable(); const key = exp._persistenceKeyName( PERSISTENCE_KEY, auth.config.apiKey, auth.name ); - if (win?.sessionStorage) { - win.sessionStorage.setItem(key, auth._getPersistence()); + if (session) { + session.setItem(key, auth._getPersistence()); } } @@ -100,13 +99,13 @@ export function _getPersistencesFromRedirect( apiKey: string, appName: string ): exp.Persistence[] { - const win = getSelfWindow(); - if (!win?.sessionStorage) { + const session = getSessionStorageIfAvailable(); + if (!session) { return []; } const key = exp._persistenceKeyName(PERSISTENCE_KEY, apiKey, appName); - const persistence = win.sessionStorage.getItem(key); + const persistence = session.getItem(key); switch (persistence) { case Persistence.NONE: @@ -120,6 +119,11 @@ export function _getPersistencesFromRedirect( } } -function getSelfWindow(): Window | null { - return typeof window !== 'undefined' ? window : null; +/** Returns session storage, or null if the property access errors */ +function getSessionStorageIfAvailable(): Storage | null { + try { + return _getSelfWindow()?.sessionStorage || null; + } catch (e) { + return null; + } } diff --git a/packages/auth-compat/src/platform.ts b/packages/auth-compat/src/platform.ts index f98deb7310a..dc0c5d9ca0c 100644 --- a/packages/auth-compat/src/platform.ts +++ b/packages/auth-compat/src/platform.ts @@ -171,3 +171,7 @@ export async function _isCordova(): Promise { }); }); } + +export function _getSelfWindow(): Window | null { + return typeof window !== 'undefined' ? window : null; +}