Skip to content

Commit 53f43f0

Browse files
committed
fix(app-check): add fallback for crypto.randomUUID() for debug tokens
1 parent 7a7634f commit 53f43f0

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

.changeset/pink-meals-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/app-check': patch
3+
---
4+
5+
[fixed] Fall back to non-cryptographically secure UUID generator for debug tokens in non secure contexts.

packages/app-check/src/storage.test.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616
*/
1717

1818
import '../test/setup';
19-
import { writeTokenToStorage, readTokenFromStorage } from './storage';
19+
import {
20+
writeTokenToStorage,
21+
readTokenFromStorage,
22+
readOrCreateDebugTokenFromStorage
23+
} from './storage';
2024
import * as indexeddbOperations from './indexeddb';
2125
import { getFakeApp } from '../test/util';
2226
import * as util from '@firebase/util';
2327
import { logger } from './logger';
2428
import { expect } from 'chai';
25-
import { stub } from 'sinon';
29+
import { spy, stub } from 'sinon';
30+
import { assert } from 'console';
2631

2732
describe('Storage', () => {
2833
const app = getFakeApp();
@@ -67,4 +72,55 @@ describe('Storage', () => {
6772
expect(warnStub.args[0][0]).to.include('something went wrong!');
6873
warnStub.restore();
6974
});
75+
76+
describe('readOrCreateDebugTokenFromStorage', () => {
77+
it('returns the existing token when it exists in IndexedDB', async () => {
78+
stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves(
79+
'existing-token'
80+
);
81+
stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves();
82+
const mathRandomSpy = spy(Math, 'random');
83+
const randomUUIDSpy = spy(self.crypto, 'randomUUID');
84+
85+
const token = await readOrCreateDebugTokenFromStorage();
86+
expect(token).to.equal('existing-token');
87+
88+
expect(randomUUIDSpy).to.not.have.been.called;
89+
expect(mathRandomSpy).to.not.have.been.called;
90+
});
91+
92+
it('does not fall back to Math.random when crypto.randomUUID exists', async () => {
93+
stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves(
94+
undefined
95+
);
96+
stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves();
97+
const mathRandomSpy = spy(Math, 'random');
98+
const randomUUIDSpy = spy(self.crypto, 'randomUUID');
99+
100+
assert(typeof crypto.randomUUID !== 'undefined');
101+
102+
await readOrCreateDebugTokenFromStorage();
103+
104+
// Verify the correct generator was used and the fallback was not
105+
expect(randomUUIDSpy).to.have.been.called;
106+
expect(mathRandomSpy).to.not.have.been.called;
107+
});
108+
109+
it('falls back to non-cryptographically secure UUID generator if crypto.randomUUID() is undefined', async () => {
110+
stub(indexeddbOperations, 'readDebugTokenFromIndexedDB').resolves(
111+
undefined
112+
);
113+
stub(indexeddbOperations, 'writeDebugTokenToIndexedDB').resolves();
114+
stub(self.crypto, 'randomUUID').value(undefined);
115+
const mathRandomSpy = spy(Math, 'random');
116+
const logSpy = spy(logger, 'warn');
117+
118+
await readOrCreateDebugTokenFromStorage();
119+
120+
expect(mathRandomSpy.called).to.be.true;
121+
expect(logSpy).to.have.been.calledWith(
122+
`crypto.randomUUID() was undefined. This happens in non secure contexts. Falling back to non-cryptographically secure UUID generator.`
123+
);
124+
});
125+
});
70126
});

packages/app-check/src/storage.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,22 @@ export async function readOrCreateDebugTokenFromStorage(): Promise<string> {
7777

7878
if (!existingDebugToken) {
7979
// create a new debug token
80-
// This function is only available in secure contexts. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
81-
const newToken = crypto.randomUUID();
80+
let newToken: string;
81+
if (typeof crypto.randomUUID !== 'undefined') {
82+
// This function is only available in secure contexts. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
83+
newToken = crypto.randomUUID();
84+
} else {
85+
// If crypto.randomUUID is undefined, we're likely in a non secure context. This can happen
86+
// when users are testing their code that isn't on their host, via HTTP without TLS.
87+
logger.warn(
88+
`crypto.randomUUID() was undefined. This happens in non secure contexts. Falling back to non-cryptographically secure UUID generator.`
89+
);
90+
newToken = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
91+
const r = (Math.random() * 16) | 0,
92+
v = c === 'x' ? r : (r & 0x3) | 0x8;
93+
return v.toString(16);
94+
});
95+
}
8296
// We don't need to block on writing to indexeddb
8397
// In case persistence failed, a new debug token will be generated every time the page is refreshed.
8498
// It renders the debug token useless because you have to manually register(whitelist) the new token in the firebase console again and again.

0 commit comments

Comments
 (0)