Skip to content

Commit

Permalink
feat(app-check): add custom factory/provider; supports all providers
Browse files Browse the repository at this point in the history
On Android this allows use of Play Integrity provider.
On iOS this allows use of AppAttest provider.

On all platforms, things should be dynamically reconfigurable if you use our custom
provider, and it should be easier to specify debug tokens for CI etc
  • Loading branch information
mikehardy committed Feb 2, 2023
1 parent 51f26d1 commit ee7df85
Show file tree
Hide file tree
Showing 17 changed files with 795 additions and 99 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
"tests:emulator:start": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh --no-daemon",
"tests:emulator:start:windows": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.bat --no-daemon",
"tests:emulator:start-ci": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
"tests:android:build": "cd tests && cross-env FIREBASE_APP_CHECK_DEBUG_TOKEN=\"698956B2-187B-49C6-9E25-C3F3530EEBAF\" yarn detox build --configuration android.emu.debug",
"tests:android:build:windows": "cd tests && cross-env FIREBASE_APP_CHECK_DEBUG_TOKEN=\"698956B2-187B-49C6-9E25-C3F3530EEBAF\" yarn detox build --configuration android.emu.debug.windows",
"tests:android:build": "cd tests && yarn detox build --configuration android.emu.debug",
"tests:android:build:windows": "cd tests && yarn detox build --configuration android.emu.debug.windows",
"tests:android:build-release": "cd tests && yarn detox build --configuration android.emu.release",
"tests:android:test": "yarn tests:android:emulator:forward && cd tests && yarn detox test --configuration android.emu.debug",
"tests:android:test:debug": "yarn tests:android:emulator:forward && cd tests && yarn detox test --configuration android.emu.debug --inspect",
Expand Down
1 change: 1 addition & 0 deletions packages/app-check/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ repositories {
dependencies {
api appProject
implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion('firebase', 'bom')}")
implementation 'com.google.firebase:firebase-appcheck-playintegrity'
implementation "com.google.firebase:firebase-appcheck-safetynet"
implementation "com.google.firebase:firebase-appcheck-debug"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,65 +23,125 @@
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.ReactNativeFirebaseJSON;
import io.invertase.firebase.common.ReactNativeFirebaseMeta;
import io.invertase.firebase.common.ReactNativeFirebaseModule;
import java.lang.reflect.*;
import io.invertase.firebase.common.ReactNativeFirebasePreferences;

public class ReactNativeFirebaseAppCheckModule extends ReactNativeFirebaseModule {
private static final String TAG = "AppCheck";
private static final String LOGTAG = "RNFBAppCheck";
private static final String KEY_APPCHECK_TOKEN_REFRESH_ENABLED = "app_check_token_auto_refresh";
ReactNativeFirebaseAppCheckProviderFactory providerFactory =
new ReactNativeFirebaseAppCheckProviderFactory();

static boolean isAppCheckTokenRefreshEnabled() {
boolean enabled;
ReactNativeFirebaseJSON json = ReactNativeFirebaseJSON.getSharedInstance();
ReactNativeFirebaseMeta meta = ReactNativeFirebaseMeta.getSharedInstance();
ReactNativeFirebasePreferences prefs = ReactNativeFirebasePreferences.getSharedInstance();

if (prefs.contains(KEY_APPCHECK_TOKEN_REFRESH_ENABLED)) {
enabled = prefs.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBPreferences: " + enabled);
} else if (json.contains(KEY_APPCHECK_TOKEN_REFRESH_ENABLED)) {
enabled = json.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBJSON: " + enabled);
} else {
enabled = meta.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBMeta: " + enabled);
}

if (BuildConfig.DEBUG) {
if (!json.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, false)) {
enabled = false;
}
Log.d(
LOGTAG,
"isAppCheckTokenRefreshEnabled after checking "
+ KEY_APPCHECK_TOKEN_REFRESH_ENABLED
+ ": "
+ enabled);
}

Log.d(LOGTAG, "isAppCheckTokenRefreshEnabled final value: " + enabled);
return enabled;
}

private boolean isAppDebuggable() throws Exception {
boolean isDebuggable = false;
PackageManager pm = getContext().getPackageManager();
if (pm != null) {
isDebuggable =
(0
!= (pm.getApplicationInfo(getContext().getPackageName(), 0).flags
& ApplicationInfo.FLAG_DEBUGGABLE));
}
Log.d(LOGTAG, "debuggable status? " + isDebuggable);
return isDebuggable;
}

ReactNativeFirebaseAppCheckModule(ReactApplicationContext reactContext) {
super(reactContext, TAG);

// Our default token refresh config comes from config files, set it
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
firebaseAppCheck.setTokenAutoRefreshEnabled(isAppCheckTokenRefreshEnabled());
}

@ReactMethod
public void configureProvider(
String appName, String providerName, String debugToken, Promise promise) {
Log.d(
LOGTAG,
"configureProvider - appName/providerName/debugToken: "
+ appName
+ "/"
+ providerName
+ (debugToken != null ? "/(not shown)" : "/null"));
try {
providerFactory.configure(appName, providerName, debugToken);
FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName))
.installAppCheckProviderFactory(providerFactory);
promise.resolve(null);
} catch (Exception e) {
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", e.getMessage());
}
}

@ReactMethod
public void activate(
String appName, String siteKeyProvider, boolean isTokenAutoRefreshEnabled, Promise promise) {
try {
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
firebaseAppCheck.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled);
boolean isDebuggable = false;
PackageManager pm = getContext().getPackageManager();
if (pm != null) {
isDebuggable =
(0
!= (pm.getApplicationInfo(getContext().getPackageName(), 0).flags
& ApplicationInfo.FLAG_DEBUGGABLE));
}

if (isDebuggable) {
FirebaseAppCheck firebaseAppCheck =
FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName));
firebaseAppCheck.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled);

// Configure our new proxy factory in a backwards-compatible way for old API
if (isAppDebuggable()) {
Log.d(LOGTAG, "app is debuggable, configuring AppCheck for testing mode");
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));
Log.d(LOGTAG, "debug app check token found in BuildConfig. Installing known token.");
providerFactory.configure(appName, "debug", BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN);
} else {
firebaseAppCheck.installAppCheckProviderFactory(
DebugAppCheckProviderFactory.getInstance());
Log.d(
LOGTAG,
"no debug app check token found in BuildConfig. Check Log for dynamic test token to"
+ " configure in console.");
providerFactory.configure(appName, "debug", null);
}

} else {
firebaseAppCheck.installAppCheckProviderFactory(
SafetyNetAppCheckProviderFactory.getInstance());
providerFactory.configure(appName, "safetyNet", null);
}

FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName))
.installAppCheckProviderFactory(providerFactory);
promise.resolve(null);
} catch (Exception e) {
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", "unimplemented");
return;
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", e.getMessage());
}
promise.resolve(null);
}

@ReactMethod
Expand All @@ -92,6 +152,7 @@ public void setTokenAutoRefreshEnabled(String appName, boolean isTokenAutoRefres

@ReactMethod
public void getToken(String appName, boolean forceRefresh, Promise promise) {
Log.d(LOGTAG, "getToken appName/forceRefresh: " + appName + "/" + forceRefresh);
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
Tasks.call(
getExecutor(),
Expand All @@ -108,7 +169,7 @@ public void getToken(String appName, boolean forceRefresh, Promise promise) {
promise.resolve(tokenResultMap);
} else {
Log.e(
TAG,
LOGTAG,
"RNFB: Unknown error while fetching AppCheck token "
+ task.getException().getMessage());
rejectPromiseWithCodeAndMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.invertase.firebase.appcheck;

/*
* Copyright (c) 2023-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import android.util.Log;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.appcheck.AppCheckProvider;
import com.google.firebase.appcheck.AppCheckProviderFactory;
import com.google.firebase.appcheck.AppCheckToken;
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory;
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory;
import java.lang.reflect.Constructor;

// Facade for all possible provider factory delegates,
// configurable dynamically instead of at startup
public class ReactNativeFirebaseAppCheckProvider implements AppCheckProvider {
private static final String LOGTAG = "RNFBAppCheck";

AppCheckProvider delegateProvider;

@Override
public Task<AppCheckToken> getToken() {
Log.d(LOGTAG, "Provider::getToken - delegating to native provider");
return delegateProvider.getToken();
}

public void configure(String appName, String providerName, String debugToken) {
Log.d(
LOGTAG,
"Provider::configure with appName/providerName/debugToken: "
+ appName
+ "/"
+ providerName
+ "/"
+ (debugToken != null ? "(not shown)" : "null"));

try {
FirebaseApp app = FirebaseApp.getInstance(appName);

if ("debug".equals(providerName)) {

// the debug configuration may have a token, or may not
if (debugToken != null) {
// Create a debug provider using hidden factory constructor and our debug token
Class<DebugAppCheckProviderFactory> debugACFactoryClass =
DebugAppCheckProviderFactory.class;
Class<?>[] argType = {String.class};
Constructor c = debugACFactoryClass.getDeclaredConstructor(argType);
Object[] cArgs = {debugToken};
delegateProvider = ((AppCheckProviderFactory) c.newInstance(cArgs)).create(app);
} else {
delegateProvider = DebugAppCheckProviderFactory.getInstance().create(app);
}
}

if ("safetyNet".equals(providerName)) {
delegateProvider = SafetyNetAppCheckProviderFactory.getInstance().create(app);
}

if ("playIntegrity".equals(providerName)) {
delegateProvider = PlayIntegrityAppCheckProviderFactory.getInstance().create(app);
}
} catch (Exception e) {
// This will bubble up and result in a rejected promise with the underlying message
throw new RuntimeException(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.invertase.firebase.appcheck;

/*
* Copyright (c) 2023-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import android.util.Log;
import com.google.firebase.FirebaseApp;
import com.google.firebase.appcheck.AppCheckProvider;
import com.google.firebase.appcheck.AppCheckProviderFactory;
import java.util.HashMap;
import java.util.Map;

public class ReactNativeFirebaseAppCheckProviderFactory implements AppCheckProviderFactory {
private static final String LOGTAG = "RNFBAppCheck";

// This object has one job - create + maintain control over one provider per app
public Map<String, ReactNativeFirebaseAppCheckProvider> providers = new HashMap();

// Our provider will serve as a facade to all the supported native providers,
// we will just pass through configuration calls to it
public void configure(String appName, String providerName, String debugToken) {
Log.d(
LOGTAG,
"ProviderFactory::configure - appName/providerName/debugToken: "
+ appName
+ "/"
+ providerName
+ (debugToken != null ? "/(not shown)" : "/null"));

ReactNativeFirebaseAppCheckProvider provider = null;

// Look up the correct provider for the given appName, create it if not created
provider = providers.get(appName);
if (provider == null) {
provider = new ReactNativeFirebaseAppCheckProvider();
providers.put(appName, provider);
}
provider.configure(appName, providerName, debugToken);
}

public AppCheckProvider create(FirebaseApp firebaseApp) {
String appName = firebaseApp.getName();
Log.d(LOGTAG, "ProviderFactory::create - fetching provider for app " + appName);
ReactNativeFirebaseAppCheckProvider provider = providers.get(appName);
if (provider == null) {
Log.d(LOGTAG, "ProviderFactory::create - provider not configured for this app.");
throw new RuntimeException(
"ReactNativeFirebaseAppCheckProvider not configured for app " + appName);
}
return provider;
}
}
Loading

0 comments on commit ee7df85

Please sign in to comment.