Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Other): add App Check support #7905

Merged
merged 1 commit into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading