Skip to content

Commit ab428f6

Browse files
authored
Add cached token changes to v9 appcheck (#5226)
1 parent f0f474b commit ab428f6

File tree

4 files changed

+55
-18
lines changed

4 files changed

+55
-18
lines changed

packages-exp/app-check-exp/src/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ import { AppCheckProvider, ListenerType } from './types';
3131
import {
3232
getToken as getTokenInternal,
3333
addTokenListener,
34-
removeTokenListener
34+
removeTokenListener,
35+
isValid
3536
} from './internal-api';
37+
import { readTokenFromStorage } from './storage';
3638

3739
declare module '@firebase/component' {
3840
interface NameServiceMapping {
@@ -95,7 +97,13 @@ function _activate(
9597
const state = getState(app);
9698

9799
const newState: AppCheckState = { ...state, activated: true };
98-
newState.provider = provider;
100+
newState.provider = provider; // Read cached token from storage if it exists and store it in memory.
101+
newState.cachedTokenPromise = readTokenFromStorage(app).then(cachedToken => {
102+
if (cachedToken && isValid(cachedToken)) {
103+
setState(app, { ...getState(app), token: cachedToken });
104+
}
105+
return cachedToken;
106+
});
99107

100108
// Use value of global `automaticDataCollectionEnabled` (which
101109
// itself defaults to false if not specified in config) if

packages-exp/app-check-exp/src/internal-api.test.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,13 @@ describe('internal api', () => {
142142
});
143143

144144
it('notifies listeners using cached token', async () => {
145+
storageReadStub.resolves(fakeCachedAppCheckToken);
145146
const appCheck = initializeAppCheck(app, {
146147
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
147148
isTokenAutoRefreshEnabled: false
148149
});
149150

150151
const clock = useFakeTimers();
151-
storageReadStub.returns(Promise.resolve(fakeCachedAppCheckToken));
152152

153153
const listener1 = spy();
154154
const listener2 = spy();
@@ -271,12 +271,12 @@ describe('internal api', () => {
271271

272272
it('loads persisted token to memory and returns it', async () => {
273273
const clock = useFakeTimers();
274+
275+
storageReadStub.resolves(fakeCachedAppCheckToken);
274276
const appCheck = initializeAppCheck(app, {
275277
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY)
276278
});
277279

278-
storageReadStub.returns(Promise.resolve(fakeCachedAppCheckToken));
279-
280280
const clientStub = stub(client, 'exchangeToken');
281281

282282
expect(getState(app).token).to.equal(undefined);
@@ -367,6 +367,10 @@ describe('internal api', () => {
367367
describe('addTokenListener', () => {
368368
it('adds token listeners', () => {
369369
const listener = (): void => {};
370+
setState(app, {
371+
...getState(app),
372+
cachedTokenPromise: Promise.resolve(undefined)
373+
});
370374

371375
addTokenListener(
372376
{ app } as AppCheckService,
@@ -379,7 +383,11 @@ describe('internal api', () => {
379383

380384
it('starts proactively refreshing token after adding the first listener', () => {
381385
const listener = (): void => {};
382-
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
386+
setState(app, {
387+
...getState(app),
388+
isTokenAutoRefreshEnabled: true,
389+
cachedTokenPromise: Promise.resolve(undefined)
390+
});
383391
expect(getState(app).tokenObservers.length).to.equal(0);
384392
expect(getState(app).tokenRefresher).to.equal(undefined);
385393

@@ -419,17 +427,15 @@ describe('internal api', () => {
419427
});
420428

421429
it('notifies the listener with the valid token in storage', done => {
430+
storageReadStub.resolves({
431+
token: `fake-cached-app-check-token`,
432+
expireTimeMillis: Date.now() + 60000,
433+
issuedAtTimeMillis: 0
434+
});
422435
const appCheck = initializeAppCheck(app, {
423436
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
424437
isTokenAutoRefreshEnabled: true
425438
});
426-
storageReadStub.returns(
427-
Promise.resolve({
428-
token: `fake-cached-app-check-token`,
429-
expireTimeMillis: Date.now() + 60000,
430-
issuedAtTimeMillis: 0
431-
})
432-
);
433439

434440
const fakeListener: AppCheckTokenListener = token => {
435441
expect(token).to.deep.equal({
@@ -449,6 +455,10 @@ describe('internal api', () => {
449455
describe('removeTokenListener', () => {
450456
it('should remove token listeners', () => {
451457
const listener = (): void => {};
458+
setState(app, {
459+
...getState(app),
460+
cachedTokenPromise: Promise.resolve(undefined)
461+
});
452462
addTokenListener(
453463
{ app } as AppCheckService,
454464
ListenerType.INTERNAL,
@@ -463,6 +473,10 @@ describe('internal api', () => {
463473
it('should stop proactively refreshing token after deleting the last listener', () => {
464474
const listener = (): void => {};
465475
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
476+
setState(app, {
477+
...getState(app),
478+
cachedTokenPromise: Promise.resolve(undefined)
479+
});
466480

467481
addTokenListener(
468482
{ app } as AppCheckService,

packages-exp/app-check-exp/src/internal-api.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { TOKEN_REFRESH_TIME } from './constants';
2828
import { Refresher } from './proactive-refresh';
2929
import { ensureActivated } from './util';
3030
import { exchangeToken, getExchangeDebugTokenRequest } from './client';
31-
import { writeTokenToStorage, readTokenFromStorage } from './storage';
31+
import { writeTokenToStorage } from './storage';
3232
import { getDebugToken, isDebugMode } from './debug';
3333
import { base64 } from '@firebase/util';
3434
import { logger } from './logger';
@@ -76,8 +76,8 @@ export async function getToken(
7676
* If there is no token in memory, try to load token from indexedDB.
7777
*/
7878
if (!token) {
79-
// readTokenFromStorage() always resolves. In case of an error, it resolves with `undefined`.
80-
const cachedToken = await readTokenFromStorage(app);
79+
// cachedTokenPromise contains the token found in IndexedDB or undefined if not found.
80+
const cachedToken = await state.cachedTokenPromise;
8181
if (cachedToken && isValid(cachedToken)) {
8282
token = cachedToken;
8383

@@ -175,14 +175,28 @@ export function addTokenListener(
175175
newState.tokenRefresher.start();
176176
}
177177

178-
// invoke the listener async immediately if there is a valid token
178+
// Invoke the listener async immediately if there is a valid token
179+
// in memory.
179180
if (state.token && isValid(state.token)) {
180181
const validToken = state.token;
181182
Promise.resolve()
182183
.then(() => listener({ token: validToken.token }))
183184
.catch(() => {
184185
/* we don't care about exceptions thrown in listeners */
185186
});
187+
} else if (state.token == null) {
188+
// Only check cache if there was no token. If the token was invalid,
189+
// skip this and rely on exchange endpoint.
190+
void state
191+
.cachedTokenPromise! // Storage token promise. Always populated in `activate()`.
192+
.then(cachedToken => {
193+
if (cachedToken && isValid(cachedToken)) {
194+
listener({ token: cachedToken.token });
195+
}
196+
})
197+
.catch(() => {
198+
/** Ignore errors in listeners. */
199+
});
186200
}
187201

188202
setState(app, newState);
@@ -288,7 +302,7 @@ function notifyTokenListeners(
288302
}
289303
}
290304

291-
function isValid(token: AppCheckTokenInternal): boolean {
305+
export function isValid(token: AppCheckTokenInternal): boolean {
292306
return token.expireTimeMillis - Date.now() > 0;
293307
}
294308

packages-exp/app-check-exp/src/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface AppCheckState {
2929
tokenObservers: AppCheckTokenObserver[];
3030
provider?: AppCheckProvider;
3131
token?: AppCheckTokenInternal;
32+
cachedTokenPromise?: Promise<AppCheckTokenInternal | undefined>;
3233
tokenRefresher?: Refresher;
3334
reCAPTCHAState?: ReCAPTCHAState;
3435
isTokenAutoRefreshEnabled?: boolean;

0 commit comments

Comments
 (0)