Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pink-meals-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/app-check': patch
---

[fixed] Fall back to non-cryptographically secure UUID generator for debug tokens in non secure contexts.
60 changes: 58 additions & 2 deletions packages/app-check/src/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.`
);
});
});
});
18 changes: 16 additions & 2 deletions packages/app-check/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,22 @@ export async function readOrCreateDebugTokenFromStorage(): Promise<string> {

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.
Expand Down
Loading