Skip to content

Commit 3550d4e

Browse files
authored
Add debug token caching to app-check-exp (#5110)
1 parent aeab9ce commit 3550d4e

File tree

6 files changed

+72
-124
lines changed

6 files changed

+72
-124
lines changed

packages-exp/app-check-exp/src/client.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ describe('client', () => {
5252
});
5353

5454
it('returns a AppCheck token', async () => {
55-
useFakeTimers();
55+
// To get a consistent expireTime/issuedAtTime.
56+
const clock = useFakeTimers();
5657
fetchStub.returns(
5758
Promise.resolve({
5859
status: 200,
@@ -77,6 +78,7 @@ describe('client', () => {
7778
expireTimeMillis: 3600,
7879
issuedAtTimeMillis: 0
7980
});
81+
clock.restore();
8082
});
8183

8284
it('throws when there is a network error', async () => {

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

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
removeTokenListener
2525
} from './internal-api';
2626
import { Provider } from '@firebase/component';
27+
import { getState } from './state';
2728

2829
/**
2930
* AppCheck Service class.
@@ -34,6 +35,10 @@ export class AppCheckService implements AppCheck, _FirebaseService {
3435
public platformLoggerProvider: Provider<'platform-logger'>
3536
) {}
3637
_delete(): Promise<void> {
38+
const { tokenObservers } = getState(this.app);
39+
for (const tokenObserver of tokenObservers) {
40+
removeTokenListener(this.app, tokenObserver.next);
41+
}
3742
return Promise.resolve();
3843
}
3944
}

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

+17-64
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,6 @@ describe('internal api', () => {
113113
fakeRecaptchaToken
114114
);
115115
expect(token).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token });
116-
// TODO: Permanently fix.
117-
// Small delay to prevent common test flakiness where this test runs
118-
// into afterEach() sometimes
119-
await new Promise(resolve => setTimeout(resolve, 50));
120116
});
121117

122118
it('resolves with a dummy token and an error if failed to get a token', async () => {
@@ -148,7 +144,7 @@ describe('internal api', () => {
148144
it('notifies listeners using cached token', async () => {
149145
const appCheck = initializeAppCheck(app, {
150146
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
151-
isTokenAutoRefreshEnabled: true
147+
isTokenAutoRefreshEnabled: false
152148
});
153149

154150
const clock = useFakeTimers();
@@ -216,6 +212,7 @@ describe('internal api', () => {
216212
});
217213

218214
it('calls 3P error handler if there is an error getting a token', async () => {
215+
stub(console, 'error');
219216
const appCheck = initializeAppCheck(app, {
220217
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
221218
isTokenAutoRefreshEnabled: true
@@ -239,6 +236,7 @@ describe('internal api', () => {
239236
});
240237

241238
it('ignores listeners that throw', async () => {
239+
stub(console, 'error');
242240
const appCheck = initializeAppCheck(app, {
243241
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
244242
isTokenAutoRefreshEnabled: true
@@ -247,9 +245,7 @@ describe('internal api', () => {
247245
stub(client, 'exchangeToken').returns(
248246
Promise.resolve(fakeRecaptchaAppCheckToken)
249247
);
250-
const listener1 = (): void => {
251-
throw new Error();
252-
};
248+
const listener1 = stub().throws(new Error());
253249
const listener2 = spy();
254250

255251
addTokenListener(
@@ -265,6 +261,9 @@ describe('internal api', () => {
265261

266262
await getToken(appCheck as AppCheckService);
267263

264+
expect(listener1).to.be.calledWith({
265+
token: fakeRecaptchaAppCheckToken.token
266+
});
268267
expect(listener2).to.be.calledWith({
269268
token: fakeRecaptchaAppCheckToken.token
270269
});
@@ -344,7 +343,7 @@ describe('internal api', () => {
344343
});
345344
});
346345

347-
it('exchanges debug token if in debug mode', async () => {
346+
it('exchanges debug token if in debug mode and there is no cached token', async () => {
348347
const exchangeTokenStub: SinonStub = stub(
349348
client,
350349
'exchangeToken'
@@ -393,15 +392,10 @@ describe('internal api', () => {
393392
expect(getState(app).tokenRefresher?.isRunning()).to.be.true;
394393
});
395394

396-
it('notifies the listener with the valid token in memory immediately', done => {
395+
it('notifies the listener with the valid token in memory immediately', async () => {
397396
const clock = useFakeTimers();
398-
const fakeListener: AppCheckTokenListener = token => {
399-
expect(token).to.deep.equal({
400-
token: `fake-memory-app-check-token`
401-
});
402-
clock.restore();
403-
done();
404-
};
397+
398+
const listener = stub();
405399

406400
setState(app, {
407401
...getState(app),
@@ -415,12 +409,16 @@ describe('internal api', () => {
415409
addTokenListener(
416410
{ app } as AppCheckService,
417411
ListenerType.INTERNAL,
418-
fakeListener
412+
listener
419413
);
414+
await clock.runAllAsync();
415+
expect(listener).to.be.calledWith({
416+
token: 'fake-memory-app-check-token'
417+
});
418+
clock.restore();
420419
});
421420

422421
it('notifies the listener with the valid token in storage', done => {
423-
const clock = useFakeTimers();
424422
const appCheck = initializeAppCheck(app, {
425423
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
426424
isTokenAutoRefreshEnabled: true
@@ -437,60 +435,15 @@ describe('internal api', () => {
437435
expect(token).to.deep.equal({
438436
token: `fake-cached-app-check-token`
439437
});
440-
clock.restore();
441-
done();
442-
};
443-
444-
addTokenListener(
445-
appCheck as AppCheckService,
446-
ListenerType.INTERNAL,
447-
fakeListener
448-
);
449-
450-
clock.tick(1);
451-
});
452-
453-
it('notifies the listener with the debug token immediately', done => {
454-
const fakeListener: AppCheckTokenListener = token => {
455-
expect(token).to.deep.equal({
456-
token: `my-debug-token`
457-
});
458438
done();
459439
};
460440

461-
const debugState = getDebugState();
462-
debugState.enabled = true;
463-
debugState.token = new Deferred();
464-
debugState.token.resolve('my-debug-token');
465-
466-
const appCheck = initializeAppCheck(app, {
467-
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY)
468-
});
469441
addTokenListener(
470442
appCheck as AppCheckService,
471443
ListenerType.INTERNAL,
472444
fakeListener
473445
);
474446
});
475-
476-
it('does NOT start token refresher in debug mode', () => {
477-
const debugState = getDebugState();
478-
debugState.enabled = true;
479-
debugState.token = new Deferred();
480-
debugState.token.resolve('my-debug-token');
481-
482-
const appCheck = initializeAppCheck(app, {
483-
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY)
484-
});
485-
addTokenListener(
486-
appCheck as AppCheckService,
487-
ListenerType.INTERNAL,
488-
() => {}
489-
);
490-
491-
const state = getState(app);
492-
expect(state.tokenRefresher).is.undefined;
493-
});
494447
});
495448

496449
describe('removeTokenListener', () => {

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

+45-55
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
ListenerType
2424
} from './types';
2525
import { AppCheckTokenListener } from './public-types';
26-
import { getDebugState, getState, setState } from './state';
26+
import { getState, setState } from './state';
2727
import { TOKEN_REFRESH_TIME } from './constants';
2828
import { Refresher } from './proactive-refresh';
2929
import { ensureActivated } from './util';
@@ -63,25 +63,17 @@ export async function getToken(
6363
): Promise<AppCheckTokenResult> {
6464
const app = appCheck.app;
6565
ensureActivated(app);
66-
/**
67-
* DEBUG MODE
68-
* return the debug token directly
69-
*/
70-
if (isDebugMode()) {
71-
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
72-
getExchangeDebugTokenRequest(app, await getDebugToken()),
73-
appCheck.platformLoggerProvider
74-
);
75-
return { token: tokenFromDebugExchange.token };
76-
}
7766

7867
const state = getState(app);
7968

69+
/**
70+
* First check if there is a token in memory from a previous `getToken()` call.
71+
*/
8072
let token: AppCheckTokenInternal | undefined = state.token;
8173
let error: Error | undefined = undefined;
8274

8375
/**
84-
* try to load token from indexedDB if it's the first time this function is called
76+
* If there is no token in memory, try to load token from indexedDB.
8577
*/
8678
if (!token) {
8779
// readTokenFromStorage() always resolves. In case of an error, it resolves with `undefined`.
@@ -95,13 +87,30 @@ export async function getToken(
9587
}
9688
}
9789

98-
// return the cached token if it's valid
90+
// Return the cached token (from either memory or indexedDB) if it's valid
9991
if (!forceRefresh && token && isValid(token)) {
10092
return {
10193
token: token.token
10294
};
10395
}
10496

97+
/**
98+
* DEBUG MODE
99+
* If debug mode is set, and there is no cached token, fetch a new App
100+
* Check token using the debug token, and return it directly.
101+
*/
102+
if (isDebugMode()) {
103+
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
104+
getExchangeDebugTokenRequest(app, await getDebugToken()),
105+
appCheck.platformLoggerProvider
106+
);
107+
// Write debug token to indexedDB.
108+
await writeTokenToStorage(app, tokenFromDebugExchange);
109+
// Write debug token to state.
110+
setState(app, { ...state, token: tokenFromDebugExchange });
111+
return { token: tokenFromDebugExchange.token };
112+
}
113+
105114
/**
106115
* request a new token
107116
*/
@@ -125,7 +134,7 @@ export async function getToken(
125134
interopTokenResult = {
126135
token: token.token
127136
};
128-
// write the new token to the memory state as well ashe persistent storage.
137+
// write the new token to the memory state as well as the persistent storage.
129138
// Only do it if we got a valid new token
130139
setState(app, { ...state, token });
131140
await writeTokenToStorage(app, token);
@@ -152,50 +161,31 @@ export function addTokenListener(
152161
...state,
153162
tokenObservers: [...state.tokenObservers, tokenObserver]
154163
};
155-
156164
/**
157-
* DEBUG MODE
158-
*
159-
* invoke the listener once with the debug token.
165+
* Invoke the listener with the valid token, then start the token refresher
160166
*/
161-
if (isDebugMode()) {
162-
const debugState = getDebugState();
163-
if (debugState.enabled && debugState.token) {
164-
debugState.token.promise
165-
.then(token => listener({ token }))
166-
.catch(() => {
167-
/* we don't care about exceptions thrown in listeners */
168-
});
169-
}
170-
} else {
171-
/**
172-
* PROD MODE
173-
*
174-
* invoke the listener with the valid token, then start the token refresher
175-
*/
176-
if (!newState.tokenRefresher) {
177-
const tokenRefresher = createTokenRefresher(appCheck);
178-
newState.tokenRefresher = tokenRefresher;
179-
}
167+
if (!newState.tokenRefresher) {
168+
const tokenRefresher = createTokenRefresher(appCheck);
169+
newState.tokenRefresher = tokenRefresher;
170+
}
180171

181-
// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
182-
// is not true.
183-
if (
184-
!newState.tokenRefresher.isRunning() &&
185-
state.isTokenAutoRefreshEnabled === true
186-
) {
187-
newState.tokenRefresher.start();
188-
}
172+
// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
173+
// is not true.
174+
if (
175+
!newState.tokenRefresher.isRunning() &&
176+
state.isTokenAutoRefreshEnabled
177+
) {
178+
newState.tokenRefresher.start();
179+
}
189180

190-
// invoke the listener async immediately if there is a valid token
191-
if (state.token && isValid(state.token)) {
192-
const validToken = state.token;
193-
Promise.resolve()
194-
.then(() => listener({ token: validToken.token }))
195-
.catch(() => {
196-
/* we don't care about exceptions thrown in listeners */
197-
});
198-
}
181+
// invoke the listener async immediately if there is a valid token
182+
if (state.token && isValid(state.token)) {
183+
const validToken = state.token;
184+
Promise.resolve()
185+
.then(() => listener({ token: validToken.token }))
186+
.catch(() => {
187+
/* we don't care about exceptions thrown in listeners */
188+
});
199189
}
200190

201191
setState(app, newState);

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

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ describe('internal api', () => {
120120
});
121121

122122
it('resolves with a dummy token and an error if failed to get a token', async () => {
123+
// getToken() errors are logged to console. Hide this during test.
123124
const errorStub = stub(console, 'error');
124125
activate(app, FAKE_SITE_KEY, true);
125126

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,7 @@ export function addTokenListener(
202202

203203
// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
204204
// is not true.
205-
if (
206-
!newState.tokenRefresher.isRunning() &&
207-
state.isTokenAutoRefreshEnabled === true
208-
) {
205+
if (!newState.tokenRefresher.isRunning() && state.isTokenAutoRefreshEnabled) {
209206
newState.tokenRefresher.start();
210207
}
211208

0 commit comments

Comments
 (0)