Skip to content

Commit

Permalink
feat(Other): add App Check support (#7905)
Browse files Browse the repository at this point in the history
  • Loading branch information
Salakar committed Jul 15, 2024
1 parent da24246 commit 753b16e
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 48 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/scripts/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
"indexes": "firestore.indexes.json"
},
"functions": {
"predeploy": [
"cd functions && yarn",
"cd functions && yarn --prefix \"$RESOURCE_DIR\" build"
],
"source": "functions"
"predeploy": ["cd functions && yarn", "cd functions && yarn --prefix \"$RESOURCE_DIR\" build"],
"source": "functions",
"ignore": [".yarn", "yarn.lock", "*.log", "node_modules"]
},
"database": {
"rules": "database.rules"
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/scripts/functions/src/fetchAppCheckToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
*
* Testing tools for invertase/react-native-firebase use only.
*
* Copyright (C) 2018-present Invertase Limited <oss@invertase.io>
*
* See License file for more information.
*/

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

// Note: this will only work in a live environment, not locally via the Firebase emulator.
export const fetchAppCheckToken = functions.https.onCall(async data => {
const { appId } = data;
const expireTimeMillis = Math.floor(Date.now() / 1000) + 60 * 60;
const result = await admin.appCheck().createToken(appId);
return { ...result, expireTimeMillis };
});
1 change: 1 addition & 0 deletions .github/workflows/scripts/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export const sleeper = functions.https.onCall(async data => {
export { testFunctionCustomRegion } from './testFunctionCustomRegion';
export { testFunctionDefaultRegion } from './testFunctionDefaultRegion';
export { testFunctionRemoteConfigUpdate } from './testFunctionRemoteConfigUpdate';
export { fetchAppCheckToken } from './fetchAppCheckToken';
171 changes: 138 additions & 33 deletions packages/app-check/e2e/appcheck.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,120 @@ function decodeJWT(token) {
return payload;
}

describe('appCheck() modular', function () {
describe('firebase v8 compatibility', function () {
before(function () {
rnfbProvider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider();
rnfbProvider.configure({
android: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
describe('appCheck()', function () {
describe('CustomProvider', function () {
if (!Platform.other) {
return;
}

it('should throw an error if no provider options are defined', function () {
try {
new firebase.appCheck.CustomProvider();
return Promise.reject(new Error('Did not throw an error.'));
} catch (e) {
e.message.should.containEql('no provider options defined');
return Promise.resolve();
}
});

it('should throw an error if no getToken function is defined', function () {
try {
new firebase.appCheck.CustomProvider({});
return Promise.reject(new Error('Did not throw an error.'));
} catch (e) {
e.message.should.containEql('no getToken function defined');
return Promise.resolve();
}
});

it('should return a token from a custom provider', async function () {
const spy = sinon.spy();
const provider = new firebase.appCheck.CustomProvider({
getToken() {
spy();
return FirebaseHelpers.fetchAppCheckToken();
},
apple: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
});

// Call from the provider directly.
const { token, expireTimeMillis } = await provider.getToken();
spy.should.be.calledOnce();
token.should.be.a.String();
expireTimeMillis.should.be.a.Number();

// Call from the app check instance.
await firebase.appCheck().initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false });
const { token: tokenFromAppCheck } = await firebase.appCheck().getToken(true);
tokenFromAppCheck.should.be.a.String();

// Confirm that app check used the custom provider getToken function.
spy.should.be.calledTwice();
});

it('should return a limited use token from a custom provider', async function () {
const provider = new firebase.appCheck.CustomProvider({
getToken() {
return FirebaseHelpers.fetchAppCheckToken();
},
web: {
provider: 'debug',
siteKey: 'none',
});

await firebase.appCheck().initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false });
const { token: tokenFromAppCheck } = await firebase.appCheck().getLimitedUseToken();
tokenFromAppCheck.should.be.a.String();
});

it('should listen for token changes', async function () {
const provider = new firebase.appCheck.CustomProvider({
getToken() {
return FirebaseHelpers.fetchAppCheckToken();
},
});

await firebase.appCheck().initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const unsubscribe = firebase.appCheck().onTokenChanged(_ => {
// TODO - improve testing cloud function to allow us to return tokens with low expiry
});

// TODO - improve testing cloud function to allow us to return tokens with low expiry
// result.should.be.an.Object();
// const { token, expireTimeMillis } = result;
// token.should.be.a.String();
// expireTimeMillis.should.be.a.Number();
unsubscribe();
});
});

describe('firebase v8 compatibility', function () {
before(function () {
let provider;

if (!Platform.other) {
provider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider();
provider.configure({
android: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
apple: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
web: {
provider: 'debug',
siteKey: 'none',
},
});
} else {
provider = new firebase.appCheck.CustomProvider({
getToken() {
return FirebaseHelpers.fetchAppCheckToken();
},
});
}

// Our tests configure a debug provider with shared secret so we should get a valid token
firebase
.appCheck()
.initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled: false });
firebase.appCheck().initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false });
});

describe('config', function () {
Expand Down Expand Up @@ -221,6 +312,10 @@ describe('appCheck() modular', function () {
});

describe('activate())', function () {
if (Platform.other) {
return;
}

it('should activate with default provider and defined token refresh', function () {
firebase
.appCheck()
Expand Down Expand Up @@ -255,25 +350,35 @@ describe('appCheck() modular', function () {
before(async function () {
const { initializeAppCheck } = appCheckModular;

rnfbProvider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider();
rnfbProvider.configure({
android: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
apple: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
web: {
provider: 'debug',
siteKey: 'none',
},
});
let provider;

if (!Platform.other) {
provider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider();
provider.configure({
android: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
apple: {
provider: 'debug',
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
},
web: {
provider: 'debug',
siteKey: 'none',
},
});
} else {
provider = new firebase.appCheck.CustomProvider({
getToken() {
return FirebaseHelpers.fetchAppCheckToken();
},
});
}

// Our tests configure a debug provider with shared secret so we should get a valid token
appCheckInstance = await initializeAppCheck(undefined, {
provider: rnfbProvider,
provider,
isTokenAutoRefreshEnabled: false,
});
});
Expand Down
30 changes: 24 additions & 6 deletions packages/app-check/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,30 @@ export namespace FirebaseAppCheckTypes {
getToken(): Promise<AppCheckToken>;
}

/**
* Custom provider class.
* @public
*/
export class CustomProvider implements AppCheckProvider {
constructor(customProviderOptions: CustomProviderOptions);
}

export interface CustomProviderOptions {
/**
* Function to get an App Check token through a custom provider
* service.
*/
getToken: () => Promise<AppCheckToken>;
}
/**
* Options for App Check initialization.
*/
export interface AppCheckOptions {
/**
* A reCAPTCHA V3 provider, reCAPTCHA Enterprise provider, or custom provider.
* Note that in react-native-firebase provider should always be ReactNativeAppCheckCustomProvider, a cross-platform
* implementation of an AppCheck CustomProvider
* The App Check provider to use. This can be either the built-in reCAPTCHA provider
* or a custom provider.
*/
provider: CustomProvider | ReCaptchaV3Provider | ReCaptchaEnterpriseProvider;
provider: CustomProvider;

/**
* If true, enables SDK to automatically
Expand Down Expand Up @@ -185,6 +199,7 @@ export namespace FirebaseAppCheckTypes {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Statics {
// firebase.appCheck.* static props go here
CustomProvider: typeof CustomProvider;
}

/**
Expand All @@ -210,18 +225,19 @@ export namespace FirebaseAppCheckTypes {
*/
export class Module extends FirebaseModule {
/**
* create a ReactNativeFirebaseAppCheckProvider option for use in react-native-firebase
* Create a ReactNativeFirebaseAppCheckProvider option for use in react-native-firebase
*/
newReactNativeFirebaseAppCheckProvider(): ReactNativeFirebaseAppCheckProvider;

/**
* initialize the AppCheck module. Note that in react-native-firebase AppCheckOptions must always
* Initialize the AppCheck module. Note that in react-native-firebase AppCheckOptions must always
* be an object with a `provider` member containing `ReactNativeFirebaseAppCheckProvider` that has returned successfully
* from a call to the `configure` method, with sub-providers for the various platforms configured to meet your project
* requirements. This must be called prior to interacting with any firebase services protected by AppCheck
*
* @param options an AppCheckOptions with a configured ReactNativeFirebaseAppCheckProvider as the provider
*/
// TODO wrong types
initializeAppCheck(options: AppCheckOptions): Promise<void>;

/**
Expand Down Expand Up @@ -282,6 +298,7 @@ export namespace FirebaseAppCheckTypes {
*
* @returns A function that unsubscribes this listener.
*/
// TODO wrong types
onTokenChanged(observer: PartialObserver<AppCheckListenerResult>): () => void;

/**
Expand All @@ -301,6 +318,7 @@ export namespace FirebaseAppCheckTypes {
*
* @returns A function that unsubscribes this listener.
*/
// TODO wrong types
onTokenChanged(
onNext: (tokenResult: AppCheckListenerResult) => void,
onError?: (error: Error) => void,
Expand Down
Loading

0 comments on commit 753b16e

Please sign in to comment.