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(app-check): android debug token argument for app-check #6026

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
32 changes: 28 additions & 4 deletions docs/app-check/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,35 @@ If unset, the "tokenAutoRefreshEnabled" setting will defer to the app's "automat

The [official documentation](https://firebase.google.com/docs/app-check/web/custom-resource) shows how to use `getToken` to access the current App Check token and then verify it in external services.

## Testing Environments / CI
## Manually Setting Up App Check Debug Token for Testing Environments / CI

### on iOS

App Check may be used in CI environments by following the upstream documentation to configure a debug token shared with your app in the CI environment.

In certain react-native testing scenarios it may be difficult to access the shared secret, but the react-native-firebase testing app for e2e testing does successfully fetch App Check tokens via:
In certain react-native testing scenarios it may be difficult to access the shared secret, but the react-native-firebase testing app for e2e testing does successfully fetch App Check tokens via setting an environment variable and initializing the debug provider before firebase configure in AppDelegate.m for iOS.

### on Android

When using a _release_ build, app-check only works when running on actual Android devices. When using a _debug_ build, you have two ways to run your application / tests with App Check support.

#### A) When testing on an actual android device (debug build)

1. Start your application on the android device.
2. Use `$adb logcat | grep DebugAppCheckProvider` to grab your temporary secret from the android logs. The output should look lit this:

D DebugAppCheckProvider: Enter this debug secret into the allow list in
the Firebase Console for your project: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

3. In the [Project Settings > App Check](https://console.firebase.google.com/project/_/settings/appcheck) section of the Firebase console, choose _Manage debug tokens_ from your app's overflow menu. Then, register the debug token you logged in the previous step.

#### B) Specifying a generated `FIREBASE_APP_CHECK_DEBUG_TOKEN` -- building for CI/CD (debug build)

When you want to test using an Android virtual device -or- when you prefer to (re)use a token of your choice -- e.g. when configuring a CI/CD pipeline -- use the following steps:

1. In the [Project Settings > App Check](https://console.firebase.google.com/project/_/settings/appcheck) section of the Firebase console, choose _Manage debug tokens_ from your app's overflow menu. Then, register a new debug token by clicking the _Add debug token_ button, then _Generate token_.
2. Pass the token you created in the previous step by supplying a `FIREBASE_APP_CHECK_DEBUG_TOKEN` environment variable to the process that build your react-native android app. e.g.:

$ FIREBASE_APP_CHECK_DEBUG_TOKEN="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" react-native run-android

- including the App Check debug test helper in the test app, along with a change to `DetoxTest` for Android
- by setting an environment variable and initializing the debug provider before firebase configure in `AppDelegate.m` for iOS.
Please note that once the android app has successfully passed the app-checks controls on the device, it will keep passing them, whether you rebuild without the secret token or not. To completely reset app-check, you must first uninstall, and then re-build / install.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"tests:packager:jet-reset-cache": "cd tests && cross-env REACT_DEBUGGER=\"echo nope\" node_modules/.bin/react-native start --reset-cache --no-interactive",
"tests:emulator:start": "cd ./.github/workflows/scripts && ./start-firebase-emulator.sh --no-daemon",
"tests:emulator:start-ci": "cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
"tests:android:build": "cd tests && yarn detox build --configuration android.emu.debug",
"tests:android:build": "cd tests && FIREBASE_APP_CHECK_DEBUG_TOKEN=\"698956B2-187B-49C6-9E25-C3F3530EEBAF\" yarn detox build --configuration android.emu.debug",
"tests:android:build-release": "cd tests && yarn detox build --configuration android.emu.release",
"tests:android:test": "cd tests && yarn detox test --configuration android.emu.debug",
"tests:android:test:debug": "cd tests && yarn detox test --configuration android.emu.debug --inspect",
Expand Down
3 changes: 3 additions & 0 deletions packages/app-check/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ project.ext {
android {
defaultConfig {
multiDexEnabled true
// Here we add a build config field to allow devs to provide a
// FIREBASE_APP_CHECK_DEBUG_TOKEN to be used in debug mode.
buildConfigField "String", "FIREBASE_APP_CHECK_DEBUG_TOKEN", "\"${System.env.FIREBASE_APP_CHECK_DEBUG_TOKEN}\""
}
lintOptions {
disable 'GradleCompatible'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
import com.facebook.react.bridge.*;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.appcheck.AppCheckProviderFactory;
import com.google.firebase.appcheck.FirebaseAppCheck;
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory;
import io.invertase.firebase.common.ReactNativeFirebaseModule;
import java.lang.reflect.*;

public class ReactNativeFirebaseAppCheckModule extends ReactNativeFirebaseModule {
private static final String TAG = "AppCheck";
Expand All @@ -51,7 +53,26 @@ public void activate(
}

if (isDebuggable) {
firebaseAppCheck.installAppCheckProviderFactory(DebugAppCheckProviderFactory.getInstance());

if (BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN != "null") {
// Get DebugAppCheckProviderFactory class
Class<DebugAppCheckProviderFactory> debugACFactoryClass =
DebugAppCheckProviderFactory.class;

// Get the (undocumented) constructor accepting a debug token as string
Class<?>[] argType = {String.class};
Constructor c = debugACFactoryClass.getDeclaredConstructor(argType);

// Create a object containing the constructor arguments
// and initialize a new instance.
Object[] cArgs = {BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN};
firebaseAppCheck.installAppCheckProviderFactory(
(AppCheckProviderFactory) c.newInstance(cArgs));
} else {
firebaseAppCheck.installAppCheckProviderFactory(
DebugAppCheckProviderFactory.getInstance());
}

} else {
firebaseAppCheck.installAppCheckProviderFactory(
SafetyNetAppCheckProviderFactory.getInstance());
Expand Down
19 changes: 1 addition & 18 deletions packages/app-check/e2e/appcheck.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('appCheck()', function () {
});
describe('getToken())', function () {
it('token fetch attempt should work', async function () {
await firebase.appCheck().activate('ignored', false);
// Our tests configure a debug provider with shared secret so we should get a valid token
const { token } = await firebase.appCheck().getToken();
token.should.not.equal('');
Expand Down Expand Up @@ -71,23 +72,5 @@ describe('appCheck()', function () {
return Promise.reject(e);
}
});
// Dynamic providers are not possible on iOS, so the debug provider is always working
it('token fetch attempt should work but fail attestation', async function () {
if (device.getPlatform() === 'android') {
try {
// Activating on Android clobbers the shared secret in the debug provider shared secret, should fail now
await firebase.appCheck().getToken(true);
return Promise.reject(
'Should have thrown after resetting shared secret on debug provider',
);
} catch (e) {
e.message.should.containEql('[appCheck/token-error]');
e.message.should.containEql('App attestation failed');
return Promise.resolve();
}
} else {
this.skip();
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,8 @@ public class DetoxTest {

@Test
public void runDetoxTests() {

try {
// 1) Detox makes it extremely difficult to pass unencoded arguments directly to the instrumentation runner on Android:
// https://github.com/wix/Detox/issues/2933
//
// 2) AppCheck will only let you set a debug AppCheck token in CI via their test helpers via instrumentation runner args
// https://firebase.google.com/docs/app-check/android/debug-provider#ci
//
// Here we avoid the Detox argument-passing / AppCheck argument-reading difficulty by directly putting the String in.
//
// This is unwanted in nearly all scenarios as it leaks an AppCheck token, but the react-native-firebase test
// project does not have AppCheck set to enforcing, so this is okay for this project.
//
// This has a great potential for leaking your token in a real app that wants to enforce and rely on AppCheck.
Class<?> testHelperClass = DebugAppCheckTestHelper.class;
Method[] methods = testHelperClass.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("fromString")) {
Method testHelperMethod = methods[i];
testHelperMethod.setAccessible(true);
DebugAppCheckTestHelper debugAppCheckTestHelper =
(DebugAppCheckTestHelper)testHelperMethod.invoke(null, "698956B2-187B-49C6-9E25-C3F3530EEBAF");
debugAppCheckTestHelper.withDebugProvider(() -> {

// This is the standard Detox androidTest implementation:
Detox.runTests(mActivityRule);

dumpCoverageData();
});
}
}
} catch (Exception e) {
throw new RuntimeException("Unable to force AppCheck debug token: ", e);
}
Detox.runTests(mActivityRule);
dumpCoverageData();
}

// If you send '-e coverage' as part of the instrumentation command line, it should dump coverage as the Instrumentation finishes.
Expand Down