Skip to content

Commit 8599d91

Browse files
authored
Change appcheck activate() to use provider pattern (#4902)
1 parent 4765182 commit 8599d91

File tree

14 files changed

+500
-128
lines changed

14 files changed

+500
-128
lines changed

.changeset/great-tigers-doubt.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/app-check': minor
3+
'@firebase/app-check-types': minor
4+
'firebase': minor
5+
---
6+
7+
Add `RecaptchaV3Provider` and `CustomProvider` classes that can be supplied to `firebase.appCheck().activate()`.

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

+24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { PartialObserver, Unsubscribe } from '@firebase/util';
1919
import { FirebaseApp } from '@firebase/app-types';
20+
import { Provider } from '@firebase/component';
2021

2122
export interface FirebaseAppCheck {
2223
/** The `FirebaseApp` associated with this instance. */
@@ -90,6 +91,29 @@ interface AppCheckProvider {
9091
getToken(): Promise<AppCheckToken>;
9192
}
9293

94+
export class ReCaptchaV3Provider {
95+
/**
96+
* @param siteKey - ReCAPTCHA v3 site key (public key).
97+
*/
98+
constructor(siteKey: string);
99+
}
100+
/*
101+
* Custom token provider.
102+
*/
103+
export class CustomProvider {
104+
/**
105+
* @param options - Options for creating the custom provider.
106+
*/
107+
constructor(options: CustomProviderOptions);
108+
}
109+
interface CustomProviderOptions {
110+
/**
111+
* Function to get an App Check token through a custom provider
112+
* service.
113+
*/
114+
getToken: () => Promise<AppCheckToken>;
115+
}
116+
93117
/**
94118
* The token returned from an `AppCheckProvider`.
95119
*/

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

+78-17
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import * as client from './client';
3939
import * as storage from './storage';
4040
import * as logger from './logger';
4141
import * as util from './util';
42+
import { ReCaptchaV3Provider } from './providers';
4243

4344
describe('api', () => {
4445
beforeEach(() => {
@@ -53,47 +54,92 @@ describe('api', () => {
5354

5455
it('sets activated to true', () => {
5556
expect(getState(app).activated).to.equal(false);
56-
activate(app, FAKE_SITE_KEY);
57+
activate(
58+
app,
59+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
60+
getFakePlatformLoggingProvider()
61+
);
5762
expect(getState(app).activated).to.equal(true);
5863
});
5964

6065
it('isTokenAutoRefreshEnabled value defaults to global setting', () => {
6166
app = getFakeApp({ automaticDataCollectionEnabled: false });
62-
activate(app, FAKE_SITE_KEY);
67+
activate(
68+
app,
69+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
70+
getFakePlatformLoggingProvider()
71+
);
6372
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(false);
6473
});
6574

6675
it('sets isTokenAutoRefreshEnabled correctly, overriding global setting', () => {
6776
app = getFakeApp({ automaticDataCollectionEnabled: false });
68-
activate(app, FAKE_SITE_KEY, true);
77+
activate(
78+
app,
79+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
80+
getFakePlatformLoggingProvider(),
81+
true
82+
);
6983
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true);
7084
});
7185

7286
it('can only be called once', () => {
73-
activate(app, FAKE_SITE_KEY);
74-
expect(() => activate(app, FAKE_SITE_KEY)).to.throw(
75-
/AppCheck can only be activated once/
87+
activate(
88+
app,
89+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
90+
getFakePlatformLoggingProvider()
7691
);
92+
expect(() =>
93+
activate(
94+
app,
95+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
96+
getFakePlatformLoggingProvider()
97+
)
98+
).to.throw(/AppCheck can only be activated once/);
7799
});
78100

79-
it('initialize reCAPTCHA when a sitekey is provided', () => {
101+
it('initialize reCAPTCHA when a sitekey string is provided', () => {
80102
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize').returns(
81103
Promise.resolve({} as any)
82104
);
83-
activate(app, FAKE_SITE_KEY);
105+
activate(app, FAKE_SITE_KEY, getFakePlatformLoggingProvider());
84106
expect(initReCAPTCHAStub).to.have.been.calledWithExactly(
85107
app,
86108
FAKE_SITE_KEY
87109
);
88110
});
89111

90-
it('does NOT initialize reCAPTCHA when a custom token provider is provided', () => {
91-
const fakeCustomTokenProvider = getFakeCustomTokenProvider();
92-
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize');
93-
activate(app, fakeCustomTokenProvider);
94-
expect(getState(app).customProvider).to.equal(fakeCustomTokenProvider);
95-
expect(initReCAPTCHAStub).to.have.not.been.called;
112+
it('initialize reCAPTCHA when a ReCaptchaV3Provider instance is provided', () => {
113+
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize').returns(
114+
Promise.resolve({} as any)
115+
);
116+
activate(
117+
app,
118+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
119+
getFakePlatformLoggingProvider()
120+
);
121+
expect(initReCAPTCHAStub).to.have.been.calledWithExactly(
122+
app,
123+
FAKE_SITE_KEY
124+
);
96125
});
126+
127+
it(
128+
'creates CustomProvider instance if user provides an object containing' +
129+
' a getToken() method',
130+
async () => {
131+
const fakeCustomTokenProvider = getFakeCustomTokenProvider();
132+
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize');
133+
activate(
134+
app,
135+
fakeCustomTokenProvider,
136+
getFakePlatformLoggingProvider()
137+
);
138+
const result = await getState(app).provider?.getToken();
139+
expect(result?.token).to.equal('fake-custom-app-check-token');
140+
expect(initReCAPTCHAStub).to.have.not.been.called;
141+
}
142+
);
97143
});
98144
describe('setTokenAutoRefreshEnabled()', () => {
99145
it('sets isTokenAutoRefreshEnabled correctly', () => {
@@ -149,7 +195,12 @@ describe('api', () => {
149195
});
150196
it('Listeners work when using top-level parameters pattern', async () => {
151197
const app = getFakeApp();
152-
activate(app, FAKE_SITE_KEY, false);
198+
activate(
199+
app,
200+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
201+
fakePlatformLoggingProvider,
202+
false
203+
);
153204
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
154205
stub(client, 'exchangeToken').returns(
155206
Promise.resolve(fakeRecaptchaAppCheckToken)
@@ -193,7 +244,12 @@ describe('api', () => {
193244

194245
it('Listeners work when using Observer pattern', async () => {
195246
const app = getFakeApp();
196-
activate(app, FAKE_SITE_KEY, false);
247+
activate(
248+
app,
249+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
250+
fakePlatformLoggingProvider,
251+
false
252+
);
197253
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
198254
stub(client, 'exchangeToken').returns(
199255
Promise.resolve(fakeRecaptchaAppCheckToken)
@@ -238,7 +294,12 @@ describe('api', () => {
238294
it('onError() catches token errors', async () => {
239295
stub(logger.logger, 'error');
240296
const app = getFakeApp();
241-
activate(app, FAKE_SITE_KEY, false);
297+
activate(
298+
app,
299+
new ReCaptchaV3Provider(FAKE_SITE_KEY),
300+
fakePlatformLoggingProvider,
301+
false
302+
);
242303
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
243304
stub(client, 'exchangeToken').rejects('exchange error');
244305

packages/app-check/src/api.ts

+34-12
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,35 @@ import {
2121
} from '@firebase/app-check-types';
2222
import { FirebaseApp } from '@firebase/app-types';
2323
import { ERROR_FACTORY, AppCheckError } from './errors';
24-
import { initialize as initializeRecaptcha } from './recaptcha';
2524
import { getState, setState, AppCheckState, ListenerType } from './state';
2625
import {
2726
getToken as getTokenInternal,
2827
addTokenListener,
29-
removeTokenListener
28+
removeTokenListener,
29+
isValid
3030
} from './internal-api';
3131
import { Provider } from '@firebase/component';
3232
import { ErrorFn, NextFn, PartialObserver, Unsubscribe } from '@firebase/util';
33+
import { CustomProvider, ReCaptchaV3Provider } from './providers';
34+
import { readTokenFromStorage } from './storage';
3335

3436
/**
3537
*
3638
* @param app
3739
* @param siteKeyOrProvider - optional custom attestation provider
38-
* or reCAPTCHA siteKey
40+
* or reCAPTCHA provider
3941
* @param isTokenAutoRefreshEnabled - if true, enables auto refresh
4042
* of appCheck token.
4143
*/
4244
export function activate(
4345
app: FirebaseApp,
44-
siteKeyOrProvider: string | AppCheckProvider,
46+
siteKeyOrProvider:
47+
| ReCaptchaV3Provider
48+
| CustomProvider
49+
// This is the old interface for users to supply a custom provider.
50+
| AppCheckProvider
51+
| string,
52+
platformLoggerProvider: Provider<'platform-logger'>,
4553
isTokenAutoRefreshEnabled?: boolean
4654
): void {
4755
const state = getState(app);
@@ -52,10 +60,29 @@ export function activate(
5260
}
5361

5462
const newState: AppCheckState = { ...state, activated: true };
63+
64+
// Read cached token from storage if it exists and store it in memory.
65+
newState.cachedTokenPromise = readTokenFromStorage(app).then(cachedToken => {
66+
if (cachedToken && isValid(cachedToken)) {
67+
setState(app, { ...getState(app), token: cachedToken });
68+
}
69+
return cachedToken;
70+
});
71+
5572
if (typeof siteKeyOrProvider === 'string') {
56-
newState.siteKey = siteKeyOrProvider;
73+
newState.provider = new ReCaptchaV3Provider(siteKeyOrProvider);
74+
} else if (
75+
siteKeyOrProvider instanceof ReCaptchaV3Provider ||
76+
siteKeyOrProvider instanceof CustomProvider
77+
) {
78+
newState.provider = siteKeyOrProvider;
5779
} else {
58-
newState.customProvider = siteKeyOrProvider;
80+
// Process "old" custom provider to avoid breaking previous users.
81+
// This was defined at beta release as simply an object with a
82+
// getToken() method.
83+
newState.provider = new CustomProvider({
84+
getToken: siteKeyOrProvider.getToken
85+
});
5986
}
6087

6188
// Use value of global `automaticDataCollectionEnabled` (which
@@ -68,12 +95,7 @@ export function activate(
6895

6996
setState(app, newState);
7097

71-
// initialize reCAPTCHA if siteKey is provided
72-
if (newState.siteKey) {
73-
initializeRecaptcha(app, newState.siteKey).catch(() => {
74-
/* we don't care about the initialization result in activate() */
75-
});
76-
}
98+
newState.provider.initialize(app, platformLoggerProvider);
7799
}
78100

79101
export function setTokenAutoRefreshEnabled(

packages/app-check/src/factory.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,15 @@ export function factory(
4646
return {
4747
app,
4848
activate: (
49-
siteKeyOrProvider: string | AppCheckProvider,
49+
siteKeyOrProvider: AppCheckProvider | string,
5050
isTokenAutoRefreshEnabled?: boolean
51-
) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled),
51+
) =>
52+
activate(
53+
app,
54+
siteKeyOrProvider,
55+
platformLoggerProvider,
56+
isTokenAutoRefreshEnabled
57+
),
5258
setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) =>
5359
setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled),
5460
getToken: forceRefresh =>

packages/app-check/src/index.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ import {
2323
} from '@firebase/component';
2424
import {
2525
FirebaseAppCheck,
26-
AppCheckComponentName
26+
AppCheckComponentName,
27+
ReCaptchaV3Provider,
28+
CustomProvider
2729
} from '@firebase/app-check-types';
2830
import { factory, internalFactory } from './factory';
31+
import {
32+
ReCaptchaV3Provider as ReCaptchaV3ProviderImpl,
33+
CustomProvider as CustomProviderImpl
34+
} from './providers';
2935
import { initializeDebugMode } from './debug';
3036
import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types';
3137
import { name, version } from '../package.json';
@@ -46,6 +52,10 @@ function registerAppCheck(firebase: _FirebaseNamespace): void {
4652
},
4753
ComponentType.PUBLIC
4854
)
55+
.setServiceProps({
56+
ReCaptchaV3Provider: ReCaptchaV3ProviderImpl,
57+
CustomProvider: CustomProviderImpl
58+
})
4959
/**
5060
* AppCheck can only be initialized by explicitly calling firebase.appCheck()
5161
* We don't want firebase products that consume AppCheck to gate on AppCheck
@@ -94,6 +104,8 @@ initializeDebugMode();
94104
declare module '@firebase/app-types' {
95105
interface FirebaseNamespace {
96106
appCheck(app?: FirebaseApp): FirebaseAppCheck;
107+
ReCaptchaV3Provider: typeof ReCaptchaV3Provider;
108+
CustomProvider: typeof CustomProvider;
97109
}
98110
interface FirebaseApp {
99111
appCheck(): FirebaseAppCheck;

0 commit comments

Comments
 (0)