diff --git a/.changeset/pink-meals-move.md b/.changeset/pink-meals-move.md new file mode 100644 index 00000000000..a8c2a5d9b82 --- /dev/null +++ b/.changeset/pink-meals-move.md @@ -0,0 +1,5 @@ +--- +'@firebase/app-check': patch +--- + +[fixed] Fall back to non-cryptographically secure UUID generator for debug tokens in non secure contexts. diff --git a/packages/app-check/src/storage.test.ts b/packages/app-check/src/storage.test.ts index 763dbf3e661..035ed92eae4 100644 --- a/packages/app-check/src/storage.test.ts +++ b/packages/app-check/src/storage.test.ts @@ -16,13 +16,18 @@ */ import '../test/setup'; -import { writeTokenToStorage, readTokenFromStorage } from './storage'; +import { + writeTokenToStorage, + readTokenFromStorage, + readOrCreateDebugTokenFromStorage +} from './storage'; import * as indexeddbOperations from './indexeddb'; import { getFakeApp } from '../test/util'; import * as util from '@firebase/util'; import { logger } from './logger'; import { expect } from 'chai'; -import { stub } from 'sinon'; +import { spy, stub } from 'sinon'; +import { assert } from 'console'; describe('Storage', () => { const app = getFakeApp(); @@ -67,4 +72,55 @@ describe('Storage', () => { expect(warnStub.args[0][0]).to.include('something went wrong!'); warnStub.restore(); }); + + describe('readOrCreateDebugTokenFromStorage', () => { + it('returns the existing token when it exists in IndexedDB', async () => { + stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves( + 'existing-token' + ); + stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves(); + const mathRandomSpy = spy(Math, 'random'); + const randomUUIDSpy = spy(self.crypto, 'randomUUID'); + + const token = await readOrCreateDebugTokenFromStorage(); + expect(token).to.equal('existing-token'); + + expect(randomUUIDSpy).to.not.have.been.called; + expect(mathRandomSpy).to.not.have.been.called; + }); + + it('does not fall back to Math.random when crypto.randomUUID exists', async () => { + stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves( + undefined + ); + stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves(); + const mathRandomSpy = spy(Math, 'random'); + const randomUUIDSpy = spy(self.crypto, 'randomUUID'); + + assert(typeof crypto.randomUUID !== 'undefined'); + + await readOrCreateDebugTokenFromStorage(); + + // Verify the correct generator was used and the fallback was not + expect(randomUUIDSpy).to.have.been.called; + expect(mathRandomSpy).to.not.have.been.called; + }); + + it('falls back to non-cryptographically secure UUID generator if crypto.randomUUID() is undefined', async () => { + stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves( + undefined + ); + stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves(); + stub(self.crypto, 'randomUUID').value(undefined); + const mathRandomSpy = spy(Math, 'random'); + const logSpy = spy(logger, 'warn'); + + await readOrCreateDebugTokenFromStorage(); + + expect(mathRandomSpy.called).to.be.true; + expect(logSpy).to.have.been.calledWith( + `crypto.randomUUID() was undefined. This happens in non secure contexts. Falling back to non-cryptographically secure UUID generator.` + ); + }); + }); }); diff --git a/packages/app-check/src/storage.ts b/packages/app-check/src/storage.ts index 36f34f00e16..d4f26299ec7 100644 --- a/packages/app-check/src/storage.ts +++ b/packages/app-check/src/storage.ts @@ -77,8 +77,22 @@ export async function readOrCreateDebugTokenFromStorage(): Promise { if (!existingDebugToken) { // create a new debug token - // This function is only available in secure contexts. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts - const newToken = crypto.randomUUID(); + let newToken: string; + if (typeof crypto.randomUUID !== 'undefined') { + // This function is only available in secure contexts. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts + newToken = crypto.randomUUID(); + } else { + // If crypto.randomUUID is undefined, we're likely in a non secure context. This can happen + // when users are testing their code that isn't on their host, via HTTP without TLS. + logger.warn( + `crypto.randomUUID() was undefined. This happens in non secure contexts. Falling back to non-cryptographically secure UUID generator.` + ); + newToken = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0, + v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } // We don't need to block on writing to indexeddb // In case persistence failed, a new debug token will be generated every time the page is refreshed. // It renders the debug token useless because you have to manually register(whitelist) the new token in the firebase console again and again.