Skip to content

Commit

Permalink
Change appcheck activate() to use provider pattern (#4902)
Browse files Browse the repository at this point in the history
  • Loading branch information
hsubox76 committed Jul 30, 2021
1 parent 4765182 commit 8599d91
Show file tree
Hide file tree
Showing 14 changed files with 500 additions and 128 deletions.
7 changes: 7 additions & 0 deletions .changeset/great-tigers-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@firebase/app-check': minor
'@firebase/app-check-types': minor
'firebase': minor
---

Add `RecaptchaV3Provider` and `CustomProvider` classes that can be supplied to `firebase.appCheck().activate()`.
24 changes: 24 additions & 0 deletions packages/app-check-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import { PartialObserver, Unsubscribe } from '@firebase/util';
import { FirebaseApp } from '@firebase/app-types';
import { Provider } from '@firebase/component';

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

export class ReCaptchaV3Provider {
/**
* @param siteKey - ReCAPTCHA v3 site key (public key).
*/
constructor(siteKey: string);
}
/*
* Custom token provider.
*/
export class CustomProvider {
/**
* @param options - Options for creating the custom provider.
*/
constructor(options: CustomProviderOptions);
}
interface CustomProviderOptions {
/**
* Function to get an App Check token through a custom provider
* service.
*/
getToken: () => Promise<AppCheckToken>;
}

/**
* The token returned from an `AppCheckProvider`.
*/
Expand Down
95 changes: 78 additions & 17 deletions packages/app-check/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import * as client from './client';
import * as storage from './storage';
import * as logger from './logger';
import * as util from './util';
import { ReCaptchaV3Provider } from './providers';

describe('api', () => {
beforeEach(() => {
Expand All @@ -53,47 +54,92 @@ describe('api', () => {

it('sets activated to true', () => {
expect(getState(app).activated).to.equal(false);
activate(app, FAKE_SITE_KEY);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider()
);
expect(getState(app).activated).to.equal(true);
});

it('isTokenAutoRefreshEnabled value defaults to global setting', () => {
app = getFakeApp({ automaticDataCollectionEnabled: false });
activate(app, FAKE_SITE_KEY);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider()
);
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(false);
});

it('sets isTokenAutoRefreshEnabled correctly, overriding global setting', () => {
app = getFakeApp({ automaticDataCollectionEnabled: false });
activate(app, FAKE_SITE_KEY, true);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider(),
true
);
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true);
});

it('can only be called once', () => {
activate(app, FAKE_SITE_KEY);
expect(() => activate(app, FAKE_SITE_KEY)).to.throw(
/AppCheck can only be activated once/
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider()
);
expect(() =>
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider()
)
).to.throw(/AppCheck can only be activated once/);
});

it('initialize reCAPTCHA when a sitekey is provided', () => {
it('initialize reCAPTCHA when a sitekey string is provided', () => {
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize').returns(
Promise.resolve({} as any)
);
activate(app, FAKE_SITE_KEY);
activate(app, FAKE_SITE_KEY, getFakePlatformLoggingProvider());
expect(initReCAPTCHAStub).to.have.been.calledWithExactly(
app,
FAKE_SITE_KEY
);
});

it('does NOT initialize reCAPTCHA when a custom token provider is provided', () => {
const fakeCustomTokenProvider = getFakeCustomTokenProvider();
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize');
activate(app, fakeCustomTokenProvider);
expect(getState(app).customProvider).to.equal(fakeCustomTokenProvider);
expect(initReCAPTCHAStub).to.have.not.been.called;
it('initialize reCAPTCHA when a ReCaptchaV3Provider instance is provided', () => {
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize').returns(
Promise.resolve({} as any)
);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
getFakePlatformLoggingProvider()
);
expect(initReCAPTCHAStub).to.have.been.calledWithExactly(
app,
FAKE_SITE_KEY
);
});

it(
'creates CustomProvider instance if user provides an object containing' +
' a getToken() method',
async () => {
const fakeCustomTokenProvider = getFakeCustomTokenProvider();
const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize');
activate(
app,
fakeCustomTokenProvider,
getFakePlatformLoggingProvider()
);
const result = await getState(app).provider?.getToken();
expect(result?.token).to.equal('fake-custom-app-check-token');
expect(initReCAPTCHAStub).to.have.not.been.called;
}
);
});
describe('setTokenAutoRefreshEnabled()', () => {
it('sets isTokenAutoRefreshEnabled correctly', () => {
Expand Down Expand Up @@ -149,7 +195,12 @@ describe('api', () => {
});
it('Listeners work when using top-level parameters pattern', async () => {
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
fakePlatformLoggingProvider,
false
);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
Expand Down Expand Up @@ -193,7 +244,12 @@ describe('api', () => {

it('Listeners work when using Observer pattern', async () => {
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
fakePlatformLoggingProvider,
false
);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
Expand Down Expand Up @@ -238,7 +294,12 @@ describe('api', () => {
it('onError() catches token errors', async () => {
stub(logger.logger, 'error');
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
activate(
app,
new ReCaptchaV3Provider(FAKE_SITE_KEY),
fakePlatformLoggingProvider,
false
);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').rejects('exchange error');

Expand Down
46 changes: 34 additions & 12 deletions packages/app-check/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,35 @@ import {
} from '@firebase/app-check-types';
import { FirebaseApp } from '@firebase/app-types';
import { ERROR_FACTORY, AppCheckError } from './errors';
import { initialize as initializeRecaptcha } from './recaptcha';
import { getState, setState, AppCheckState, ListenerType } from './state';
import {
getToken as getTokenInternal,
addTokenListener,
removeTokenListener
removeTokenListener,
isValid
} from './internal-api';
import { Provider } from '@firebase/component';
import { ErrorFn, NextFn, PartialObserver, Unsubscribe } from '@firebase/util';
import { CustomProvider, ReCaptchaV3Provider } from './providers';
import { readTokenFromStorage } from './storage';

/**
*
* @param app
* @param siteKeyOrProvider - optional custom attestation provider
* or reCAPTCHA siteKey
* or reCAPTCHA provider
* @param isTokenAutoRefreshEnabled - if true, enables auto refresh
* of appCheck token.
*/
export function activate(
app: FirebaseApp,
siteKeyOrProvider: string | AppCheckProvider,
siteKeyOrProvider:
| ReCaptchaV3Provider
| CustomProvider
// This is the old interface for users to supply a custom provider.
| AppCheckProvider
| string,
platformLoggerProvider: Provider<'platform-logger'>,
isTokenAutoRefreshEnabled?: boolean
): void {
const state = getState(app);
Expand All @@ -52,10 +60,29 @@ export function activate(
}

const newState: AppCheckState = { ...state, activated: true };

// Read cached token from storage if it exists and store it in memory.
newState.cachedTokenPromise = readTokenFromStorage(app).then(cachedToken => {
if (cachedToken && isValid(cachedToken)) {
setState(app, { ...getState(app), token: cachedToken });
}
return cachedToken;
});

if (typeof siteKeyOrProvider === 'string') {
newState.siteKey = siteKeyOrProvider;
newState.provider = new ReCaptchaV3Provider(siteKeyOrProvider);
} else if (
siteKeyOrProvider instanceof ReCaptchaV3Provider ||
siteKeyOrProvider instanceof CustomProvider
) {
newState.provider = siteKeyOrProvider;
} else {
newState.customProvider = siteKeyOrProvider;
// Process "old" custom provider to avoid breaking previous users.
// This was defined at beta release as simply an object with a
// getToken() method.
newState.provider = new CustomProvider({
getToken: siteKeyOrProvider.getToken
});
}

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

setState(app, newState);

// initialize reCAPTCHA if siteKey is provided
if (newState.siteKey) {
initializeRecaptcha(app, newState.siteKey).catch(() => {
/* we don't care about the initialization result in activate() */
});
}
newState.provider.initialize(app, platformLoggerProvider);
}

export function setTokenAutoRefreshEnabled(
Expand Down
10 changes: 8 additions & 2 deletions packages/app-check/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ export function factory(
return {
app,
activate: (
siteKeyOrProvider: string | AppCheckProvider,
siteKeyOrProvider: AppCheckProvider | string,
isTokenAutoRefreshEnabled?: boolean
) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled),
) =>
activate(
app,
siteKeyOrProvider,
platformLoggerProvider,
isTokenAutoRefreshEnabled
),
setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) =>
setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled),
getToken: forceRefresh =>
Expand Down
14 changes: 13 additions & 1 deletion packages/app-check/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ import {
} from '@firebase/component';
import {
FirebaseAppCheck,
AppCheckComponentName
AppCheckComponentName,
ReCaptchaV3Provider,
CustomProvider
} from '@firebase/app-check-types';
import { factory, internalFactory } from './factory';
import {
ReCaptchaV3Provider as ReCaptchaV3ProviderImpl,
CustomProvider as CustomProviderImpl
} from './providers';
import { initializeDebugMode } from './debug';
import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types';
import { name, version } from '../package.json';
Expand All @@ -46,6 +52,10 @@ function registerAppCheck(firebase: _FirebaseNamespace): void {
},
ComponentType.PUBLIC
)
.setServiceProps({
ReCaptchaV3Provider: ReCaptchaV3ProviderImpl,
CustomProvider: CustomProviderImpl
})
/**
* AppCheck can only be initialized by explicitly calling firebase.appCheck()
* We don't want firebase products that consume AppCheck to gate on AppCheck
Expand Down Expand Up @@ -94,6 +104,8 @@ initializeDebugMode();
declare module '@firebase/app-types' {
interface FirebaseNamespace {
appCheck(app?: FirebaseApp): FirebaseAppCheck;
ReCaptchaV3Provider: typeof ReCaptchaV3Provider;
CustomProvider: typeof CustomProvider;
}
interface FirebaseApp {
appCheck(): FirebaseAppCheck;
Expand Down

0 comments on commit 8599d91

Please sign in to comment.