Skip to content

Commit 35f6928

Browse files
authored
Merge 0b4d4fa into 5ad33ab
2 parents 5ad33ab + 0b4d4fa commit 35f6928

File tree

13 files changed

+133
-39
lines changed

13 files changed

+133
-39
lines changed

packages/app-check-types/index.d.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818
export interface FirebaseAppCheck {
1919
/**
2020
* Activate AppCheck
21-
* @param siteKeyOrOrovider - reCAPTCHA sitekey or custom token provider
21+
* @param siteKeyOrProvider - reCAPTCHA sitekey or custom token provider
22+
* @param isTokenAutoRefreshEnabled - If true, enables SDK to automatically
23+
* refresh AppCheck token as needed. If undefined, the value will default
24+
* to the value of `app.automaticDataCollectionEnabled`. That property
25+
* defaults to false and can be set in the app config.
2226
*/
23-
activate(siteKeyOrProvider: string | AppCheckProvider): void;
27+
activate(
28+
siteKeyOrProvider: string | AppCheckProvider,
29+
isTokenAutoRefreshEnabled?: boolean
30+
): void;
31+
setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void;
2432
}
2533

2634
interface AppCheckProvider {

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import '../test/setup';
1818
import { expect } from 'chai';
1919
import { stub } from 'sinon';
20-
import { activate } from './api';
20+
import { activate, setTokenAutoRefreshEnabled } from './api';
2121
import {
2222
FAKE_SITE_KEY,
2323
getFakeApp,
@@ -41,6 +41,18 @@ describe('api', () => {
4141
expect(getState(app).activated).to.equal(true);
4242
});
4343

44+
it('isTokenAutoRefreshEnabled value defaults to global setting', () => {
45+
app = getFakeApp({ automaticDataCollectionEnabled: false });
46+
activate(app, FAKE_SITE_KEY);
47+
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(false);
48+
});
49+
50+
it('sets isTokenAutoRefreshEnabled correctly, overriding global setting', () => {
51+
app = getFakeApp({ automaticDataCollectionEnabled: false });
52+
activate(app, FAKE_SITE_KEY, true);
53+
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true);
54+
});
55+
4456
it('can only be called once', () => {
4557
activate(app, FAKE_SITE_KEY);
4658
expect(() => activate(app, FAKE_SITE_KEY)).to.throw(
@@ -67,4 +79,11 @@ describe('api', () => {
6779
expect(initReCAPTCHAStub).to.have.not.been.called;
6880
});
6981
});
82+
describe('setTokenAutoRefreshEnabled()', () => {
83+
it('sets isTokenAutoRefreshEnabled correctly', () => {
84+
const app = getFakeApp({ automaticDataCollectionEnabled: false });
85+
setTokenAutoRefreshEnabled(app, true);
86+
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true);
87+
});
88+
});
7089
});

packages/app-check/src/api.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ import { getState, setState, AppCheckState } from './state';
2424
/**
2525
*
2626
* @param app
27-
* @param provider - optional custom attestation provider
27+
* @param siteKeyOrProvider - optional custom attestation provider
28+
* or reCAPTCHA siteKey
29+
* @param isTokenAutoRefreshEnabled - if true, enables auto refresh
30+
* of appCheck token.
2831
*/
2932
export function activate(
3033
app: FirebaseApp,
31-
siteKeyOrProvider: string | AppCheckProvider
34+
siteKeyOrProvider: string | AppCheckProvider,
35+
isTokenAutoRefreshEnabled?: boolean
3236
): void {
3337
const state = getState(app);
3438
if (state.activated) {
@@ -44,6 +48,14 @@ export function activate(
4448
newState.customProvider = siteKeyOrProvider;
4549
}
4650

51+
// Use value of global `automaticDataCollectionEnabled` (which
52+
// itself defaults to false if not specified in config) if
53+
// `isTokenAutoRefreshEnabled` param was not provided by user.
54+
newState.isTokenAutoRefreshEnabled =
55+
isTokenAutoRefreshEnabled === undefined
56+
? app.automaticDataCollectionEnabled
57+
: isTokenAutoRefreshEnabled;
58+
4759
setState(app, newState);
4860

4961
// initialize reCAPTCHA if siteKey is provided
@@ -53,3 +65,20 @@ export function activate(
5365
});
5466
}
5567
}
68+
69+
export function setTokenAutoRefreshEnabled(
70+
app: FirebaseApp,
71+
isTokenAutoRefreshEnabled: boolean
72+
): void {
73+
const state = getState(app);
74+
// This will exist if any product libraries have called
75+
// `addTokenListener()`
76+
if (state.tokenRefresher) {
77+
if (isTokenAutoRefreshEnabled === true) {
78+
state.tokenRefresher.start();
79+
} else {
80+
state.tokenRefresher.stop();
81+
}
82+
}
83+
setState(app, { ...state, isTokenAutoRefreshEnabled });
84+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ describe('client', () => {
6969

7070
expect(response).to.deep.equal({
7171
token: 'fake-appcheck-token',
72-
expireTimeMillis: 3600
72+
expireTimeMillis: 3600,
73+
issuedAtTimeMillis: 0
7374
});
7475
});
7576

packages/app-check/src/client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
} from './constants';
2323
import { FirebaseApp } from '@firebase/app-types';
2424
import { ERROR_FACTORY, AppCheckError } from './errors';
25-
import { AppCheckToken } from '@firebase/app-check-types';
2625
import { version } from '../package.json';
26+
import { AppCheckTokenInternal } from './state';
2727

2828
/**
2929
* Response JSON returned from AppCheck server endpoint.
@@ -42,7 +42,7 @@ interface AppCheckRequest {
4242
export async function exchangeToken({
4343
url,
4444
body
45-
}: AppCheckRequest): Promise<AppCheckToken> {
45+
}: AppCheckRequest): Promise<AppCheckTokenInternal> {
4646
const options = {
4747
method: 'POST',
4848
body: JSON.stringify(body),
@@ -89,9 +89,11 @@ export async function exchangeToken({
8989
}
9090
const timeToLiveAsNumber = Number(match[1]) * 1000;
9191

92+
const now = Date.now();
9293
return {
9394
token: responseBody.attestationToken,
94-
expireTimeMillis: Date.now() + timeToLiveAsNumber
95+
expireTimeMillis: now + timeToLiveAsNumber,
96+
issuedAtTimeMillis: now
9597
};
9698
}
9799

packages/app-check/src/factory.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { FirebaseAppCheck, AppCheckProvider } from '@firebase/app-check-types';
19-
import { activate } from './api';
19+
import { activate, setTokenAutoRefreshEnabled } from './api';
2020
import { FirebaseApp } from '@firebase/app-types';
2121
import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types';
2222
import {
@@ -27,8 +27,12 @@ import {
2727

2828
export function factory(app: FirebaseApp): FirebaseAppCheck {
2929
return {
30-
activate: (siteKeyOrProvider: string | AppCheckProvider) =>
31-
activate(app, siteKeyOrProvider)
30+
activate: (
31+
siteKeyOrProvider: string | AppCheckProvider,
32+
isTokenAutoRefreshEnabled?: boolean
33+
) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled),
34+
setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) =>
35+
setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled)
3236
};
3337
}
3438

packages/app-check/src/indexeddb.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AppCheckToken } from '@firebase/app-check-types';
1918
import { FirebaseApp } from '@firebase/app-types';
2019
import { ERROR_FACTORY, AppCheckError } from './errors';
20+
import { AppCheckTokenInternal } from './state';
2121
const DB_NAME = 'firebase-app-check-database';
2222
const DB_VERSION = 1;
2323
const STORE_NAME = 'firebase-app-check-store';
@@ -74,13 +74,13 @@ function getDBPromise(): Promise<IDBDatabase> {
7474

7575
export function readTokenFromIndexedDB(
7676
app: FirebaseApp
77-
): Promise<AppCheckToken | undefined> {
78-
return read(computeKey(app)) as Promise<AppCheckToken | undefined>;
77+
): Promise<AppCheckTokenInternal | undefined> {
78+
return read(computeKey(app)) as Promise<AppCheckTokenInternal | undefined>;
7979
}
8080

8181
export function writeTokenToIndexedDB(
8282
app: FirebaseApp,
83-
token: AppCheckToken
83+
token: AppCheckTokenInternal
8484
): Promise<void> {
8585
return write(computeKey(app), token);
8686
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ describe('internal api', () => {
5656
const fakeRecaptchaToken = 'fake-recaptcha-token';
5757
const fakeRecaptchaAppCheckToken = {
5858
token: 'fake-recaptcha-app-check-token',
59-
expireTimeMillis: 123
59+
expireTimeMillis: 123,
60+
issuedAtTimeMillis: 0
6061
};
6162

6263
const fakeCachedAppCheckToken = {
6364
token: 'fake-cached-app-check-token',
64-
expireTimeMillis: 123
65+
expireTimeMillis: 123,
66+
issuedAtTimeMillis: 0
6567
};
6668

6769
it('uses customTokenProvider to get an AppCheck token', async () => {
@@ -290,6 +292,7 @@ describe('internal api', () => {
290292

291293
it('starts proactively refreshing token after adding the first listener', () => {
292294
const listener = (): void => {};
295+
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
293296
expect(getState(app).tokenListeners.length).to.equal(0);
294297
expect(getState(app).tokenRefresher).to.equal(undefined);
295298

@@ -312,7 +315,8 @@ describe('internal api', () => {
312315
...getState(app),
313316
token: {
314317
token: `fake-memory-app-check-token`,
315-
expireTimeMillis: 123
318+
expireTimeMillis: 123,
319+
issuedAtTimeMillis: 0
316320
}
317321
});
318322

@@ -325,7 +329,8 @@ describe('internal api', () => {
325329
stub(storage, 'readTokenFromStorage').returns(
326330
Promise.resolve({
327331
token: `fake-cached-app-check-token`,
328-
expireTimeMillis: 123
332+
expireTimeMillis: 123,
333+
issuedAtTimeMillis: 0
329334
})
330335
);
331336

@@ -384,6 +389,7 @@ describe('internal api', () => {
384389

385390
it('should stop proactively refreshing token after deleting the last listener', () => {
386391
const listener = (): void => {};
392+
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
387393

388394
addTokenListener(app, listener);
389395
expect(getState(app).tokenListeners.length).to.equal(1);

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ import {
2121
AppCheckTokenResult,
2222
AppCheckTokenListener
2323
} from '@firebase/app-check-interop-types';
24-
import { AppCheckToken } from '@firebase/app-check-types';
25-
import { getDebugState, getState, setState } from './state';
24+
import {
25+
AppCheckTokenInternal,
26+
getDebugState,
27+
getState,
28+
setState
29+
} from './state';
2630
import { TOKEN_REFRESH_TIME } from './constants';
2731
import { Refresher } from './proactive-refresh';
2832
import { ensureActivated } from './util';
@@ -70,15 +74,15 @@ export async function getToken(
7074
* return the debug token directly
7175
*/
7276
if (isDebugMode()) {
73-
const tokenFromDebugExchange: AppCheckToken = await exchangeToken(
77+
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
7478
getExchangeDebugTokenRequest(app, await getDebugToken())
7579
);
7680
return { token: tokenFromDebugExchange.token };
7781
}
7882

7983
const state = getState(app);
8084

81-
let token: AppCheckToken | undefined = state.token;
85+
let token: AppCheckTokenInternal | undefined = state.token;
8286
let error: Error | undefined = undefined;
8387

8488
/**
@@ -108,7 +112,8 @@ export async function getToken(
108112
*/
109113
try {
110114
if (state.customProvider) {
111-
token = await state.customProvider.getToken();
115+
const customToken = await state.customProvider.getToken();
116+
token = { ...customToken, issuedAtTimeMillis: Date.now() };
112117
} else {
113118
const attestedClaimsToken = await getReCAPTCHAToken(app).catch(_e => {
114119
// reCaptcha.execute() throws null which is not very descriptive.
@@ -178,7 +183,12 @@ export function addTokenListener(
178183
newState.tokenRefresher = tokenRefresher;
179184
}
180185

181-
if (!newState.tokenRefresher.isRunning()) {
186+
// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
187+
// is not true.
188+
if (
189+
!newState.tokenRefresher.isRunning() &&
190+
state.isTokenAutoRefreshEnabled === true
191+
) {
182192
newState.tokenRefresher.start();
183193
}
184194

@@ -245,12 +255,20 @@ function createTokenRefresher(app: FirebaseApp): Refresher {
245255
const state = getState(app);
246256

247257
if (state.token) {
248-
return Math.max(
249-
0,
250-
state.token.expireTimeMillis -
251-
Date.now() -
252-
TOKEN_REFRESH_TIME.OFFSET_DURATION
258+
// issuedAtTime + (50% * total TTL) + 5 minutes
259+
let nextRefreshTimeMillis =
260+
state.token.issuedAtTimeMillis +
261+
(state.token.expireTimeMillis - state.token.issuedAtTimeMillis) *
262+
0.5 +
263+
5 * 60 * 1000;
264+
// Do not allow refresh time to be past (expireTime - 5 minutes)
265+
const latestAllowableRefresh =
266+
state.token.expireTimeMillis - 5 * 60 * 1000;
267+
nextRefreshTimeMillis = Math.min(
268+
nextRefreshTimeMillis,
269+
latestAllowableRefresh
253270
);
271+
return Math.max(0, nextRefreshTimeMillis - Date.now());
254272
} else {
255273
return 0;
256274
}
@@ -275,7 +293,7 @@ function notifyTokenListeners(
275293
}
276294
}
277295

278-
function isValid(token: AppCheckToken): boolean {
296+
function isValid(token: AppCheckTokenInternal): boolean {
279297
return token.expireTimeMillis - Date.now() > 0;
280298
}
281299

packages/app-check/src/state.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ import { AppCheckTokenListener } from '@firebase/app-check-interop-types';
2121
import { Refresher } from './proactive-refresh';
2222
import { Deferred } from '@firebase/util';
2323
import { GreCAPTCHA } from './recaptcha';
24+
25+
export interface AppCheckTokenInternal extends AppCheckToken {
26+
issuedAtTimeMillis: number;
27+
}
2428
export interface AppCheckState {
2529
activated: boolean;
2630
tokenListeners: AppCheckTokenListener[];
2731
customProvider?: AppCheckProvider;
2832
siteKey?: string;
29-
token?: AppCheckToken;
33+
token?: AppCheckTokenInternal;
3034
tokenRefresher?: Refresher;
3135
reCAPTCHAState?: ReCAPTCHAState;
36+
isTokenAutoRefreshEnabled?: boolean;
3237
}
3338

3439
export interface ReCAPTCHAState {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ describe('Storage', () => {
2828
const app = getFakeApp();
2929
const fakeToken = {
3030
token: 'fake-app-check-token',
31-
expireTimeMillis: 345
31+
expireTimeMillis: 345,
32+
issuedAtTimeMillis: 0
3233
};
3334

3435
it('sets and gets appCheck token to indexeddb', async () => {

0 commit comments

Comments
 (0)