Skip to content

Commit ee7df85

Browse files
committed
feat(app-check): add custom factory/provider; supports all providers
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
1 parent 51f26d1 commit ee7df85

17 files changed

Lines changed: 795 additions & 99 deletions

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"tests:emulator:start": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh --no-daemon",
3131
"tests:emulator:start:windows": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.bat --no-daemon",
3232
"tests:emulator:start-ci": "yarn tests:emulator:prepare && cd ./.github/workflows/scripts && ./start-firebase-emulator.sh",
33-
"tests:android:build": "cd tests && cross-env FIREBASE_APP_CHECK_DEBUG_TOKEN=\"698956B2-187B-49C6-9E25-C3F3530EEBAF\" yarn detox build --configuration android.emu.debug",
34-
"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",
33+
"tests:android:build": "cd tests && yarn detox build --configuration android.emu.debug",
34+
"tests:android:build:windows": "cd tests && yarn detox build --configuration android.emu.debug.windows",
3535
"tests:android:build-release": "cd tests && yarn detox build --configuration android.emu.release",
3636
"tests:android:test": "yarn tests:android:emulator:forward && cd tests && yarn detox test --configuration android.emu.debug",
3737
"tests:android:test:debug": "yarn tests:android:emulator:forward && cd tests && yarn detox test --configuration android.emu.debug --inspect",

packages/app-check/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ repositories {
8989
dependencies {
9090
api appProject
9191
implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion('firebase', 'bom')}")
92+
implementation 'com.google.firebase:firebase-appcheck-playintegrity'
9293
implementation "com.google.firebase:firebase-appcheck-safetynet"
9394
implementation "com.google.firebase:firebase-appcheck-debug"
9495
}

packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,65 +23,125 @@
2323
import com.facebook.react.bridge.*;
2424
import com.google.android.gms.tasks.Tasks;
2525
import com.google.firebase.FirebaseApp;
26-
import com.google.firebase.appcheck.AppCheckProviderFactory;
2726
import com.google.firebase.appcheck.FirebaseAppCheck;
28-
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
29-
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory;
27+
import io.invertase.firebase.common.ReactNativeFirebaseJSON;
28+
import io.invertase.firebase.common.ReactNativeFirebaseMeta;
3029
import io.invertase.firebase.common.ReactNativeFirebaseModule;
31-
import java.lang.reflect.*;
30+
import io.invertase.firebase.common.ReactNativeFirebasePreferences;
3231

3332
public class ReactNativeFirebaseAppCheckModule extends ReactNativeFirebaseModule {
3433
private static final String TAG = "AppCheck";
34+
private static final String LOGTAG = "RNFBAppCheck";
35+
private static final String KEY_APPCHECK_TOKEN_REFRESH_ENABLED = "app_check_token_auto_refresh";
36+
ReactNativeFirebaseAppCheckProviderFactory providerFactory =
37+
new ReactNativeFirebaseAppCheckProviderFactory();
38+
39+
static boolean isAppCheckTokenRefreshEnabled() {
40+
boolean enabled;
41+
ReactNativeFirebaseJSON json = ReactNativeFirebaseJSON.getSharedInstance();
42+
ReactNativeFirebaseMeta meta = ReactNativeFirebaseMeta.getSharedInstance();
43+
ReactNativeFirebasePreferences prefs = ReactNativeFirebasePreferences.getSharedInstance();
44+
45+
if (prefs.contains(KEY_APPCHECK_TOKEN_REFRESH_ENABLED)) {
46+
enabled = prefs.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
47+
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBPreferences: " + enabled);
48+
} else if (json.contains(KEY_APPCHECK_TOKEN_REFRESH_ENABLED)) {
49+
enabled = json.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
50+
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBJSON: " + enabled);
51+
} else {
52+
enabled = meta.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, true);
53+
Log.d(LOGTAG, "isAppCheckCollectionEnabled via RNFBMeta: " + enabled);
54+
}
55+
56+
if (BuildConfig.DEBUG) {
57+
if (!json.getBooleanValue(KEY_APPCHECK_TOKEN_REFRESH_ENABLED, false)) {
58+
enabled = false;
59+
}
60+
Log.d(
61+
LOGTAG,
62+
"isAppCheckTokenRefreshEnabled after checking "
63+
+ KEY_APPCHECK_TOKEN_REFRESH_ENABLED
64+
+ ": "
65+
+ enabled);
66+
}
67+
68+
Log.d(LOGTAG, "isAppCheckTokenRefreshEnabled final value: " + enabled);
69+
return enabled;
70+
}
71+
72+
private boolean isAppDebuggable() throws Exception {
73+
boolean isDebuggable = false;
74+
PackageManager pm = getContext().getPackageManager();
75+
if (pm != null) {
76+
isDebuggable =
77+
(0
78+
!= (pm.getApplicationInfo(getContext().getPackageName(), 0).flags
79+
& ApplicationInfo.FLAG_DEBUGGABLE));
80+
}
81+
Log.d(LOGTAG, "debuggable status? " + isDebuggable);
82+
return isDebuggable;
83+
}
3584

3685
ReactNativeFirebaseAppCheckModule(ReactApplicationContext reactContext) {
3786
super(reactContext, TAG);
87+
88+
// Our default token refresh config comes from config files, set it
89+
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
90+
firebaseAppCheck.setTokenAutoRefreshEnabled(isAppCheckTokenRefreshEnabled());
91+
}
92+
93+
@ReactMethod
94+
public void configureProvider(
95+
String appName, String providerName, String debugToken, Promise promise) {
96+
Log.d(
97+
LOGTAG,
98+
"configureProvider - appName/providerName/debugToken: "
99+
+ appName
100+
+ "/"
101+
+ providerName
102+
+ (debugToken != null ? "/(not shown)" : "/null"));
103+
try {
104+
providerFactory.configure(appName, providerName, debugToken);
105+
FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName))
106+
.installAppCheckProviderFactory(providerFactory);
107+
promise.resolve(null);
108+
} catch (Exception e) {
109+
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", e.getMessage());
110+
}
38111
}
39112

40113
@ReactMethod
41114
public void activate(
42115
String appName, String siteKeyProvider, boolean isTokenAutoRefreshEnabled, Promise promise) {
43116
try {
44-
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
45-
firebaseAppCheck.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled);
46-
boolean isDebuggable = false;
47-
PackageManager pm = getContext().getPackageManager();
48-
if (pm != null) {
49-
isDebuggable =
50-
(0
51-
!= (pm.getApplicationInfo(getContext().getPackageName(), 0).flags
52-
& ApplicationInfo.FLAG_DEBUGGABLE));
53-
}
54117

55-
if (isDebuggable) {
118+
FirebaseAppCheck firebaseAppCheck =
119+
FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName));
120+
firebaseAppCheck.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled);
56121

122+
// Configure our new proxy factory in a backwards-compatible way for old API
123+
if (isAppDebuggable()) {
124+
Log.d(LOGTAG, "app is debuggable, configuring AppCheck for testing mode");
57125
if (BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN != "null") {
58-
// Get DebugAppCheckProviderFactory class
59-
Class<DebugAppCheckProviderFactory> debugACFactoryClass =
60-
DebugAppCheckProviderFactory.class;
61-
62-
// Get the (undocumented) constructor accepting a debug token as string
63-
Class<?>[] argType = {String.class};
64-
Constructor c = debugACFactoryClass.getDeclaredConstructor(argType);
65-
66-
// Create a object containing the constructor arguments
67-
// and initialize a new instance.
68-
Object[] cArgs = {BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN};
69-
firebaseAppCheck.installAppCheckProviderFactory(
70-
(AppCheckProviderFactory) c.newInstance(cArgs));
126+
Log.d(LOGTAG, "debug app check token found in BuildConfig. Installing known token.");
127+
providerFactory.configure(appName, "debug", BuildConfig.FIREBASE_APP_CHECK_DEBUG_TOKEN);
71128
} else {
72-
firebaseAppCheck.installAppCheckProviderFactory(
73-
DebugAppCheckProviderFactory.getInstance());
129+
Log.d(
130+
LOGTAG,
131+
"no debug app check token found in BuildConfig. Check Log for dynamic test token to"
132+
+ " configure in console.");
133+
providerFactory.configure(appName, "debug", null);
74134
}
75-
76135
} else {
77-
firebaseAppCheck.installAppCheckProviderFactory(
78-
SafetyNetAppCheckProviderFactory.getInstance());
136+
providerFactory.configure(appName, "safetyNet", null);
79137
}
138+
139+
FirebaseAppCheck.getInstance(FirebaseApp.getInstance(appName))
140+
.installAppCheckProviderFactory(providerFactory);
141+
promise.resolve(null);
80142
} catch (Exception e) {
81-
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", "unimplemented");
82-
return;
143+
rejectPromiseWithCodeAndMessage(promise, "unknown", "internal-error", e.getMessage());
83144
}
84-
promise.resolve(null);
85145
}
86146

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

93153
@ReactMethod
94154
public void getToken(String appName, boolean forceRefresh, Promise promise) {
155+
Log.d(LOGTAG, "getToken appName/forceRefresh: " + appName + "/" + forceRefresh);
95156
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
96157
Tasks.call(
97158
getExecutor(),
@@ -108,7 +169,7 @@ public void getToken(String appName, boolean forceRefresh, Promise promise) {
108169
promise.resolve(tokenResultMap);
109170
} else {
110171
Log.e(
111-
TAG,
172+
LOGTAG,
112173
"RNFB: Unknown error while fetching AppCheck token "
113174
+ task.getException().getMessage());
114175
rejectPromiseWithCodeAndMessage(
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.invertase.firebase.appcheck;
2+
3+
/*
4+
* Copyright (c) 2023-present Invertase Limited & Contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this library except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
import android.util.Log;
21+
import com.google.android.gms.tasks.Task;
22+
import com.google.firebase.FirebaseApp;
23+
import com.google.firebase.appcheck.AppCheckProvider;
24+
import com.google.firebase.appcheck.AppCheckProviderFactory;
25+
import com.google.firebase.appcheck.AppCheckToken;
26+
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
27+
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory;
28+
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory;
29+
import java.lang.reflect.Constructor;
30+
31+
// Facade for all possible provider factory delegates,
32+
// configurable dynamically instead of at startup
33+
public class ReactNativeFirebaseAppCheckProvider implements AppCheckProvider {
34+
private static final String LOGTAG = "RNFBAppCheck";
35+
36+
AppCheckProvider delegateProvider;
37+
38+
@Override
39+
public Task<AppCheckToken> getToken() {
40+
Log.d(LOGTAG, "Provider::getToken - delegating to native provider");
41+
return delegateProvider.getToken();
42+
}
43+
44+
public void configure(String appName, String providerName, String debugToken) {
45+
Log.d(
46+
LOGTAG,
47+
"Provider::configure with appName/providerName/debugToken: "
48+
+ appName
49+
+ "/"
50+
+ providerName
51+
+ "/"
52+
+ (debugToken != null ? "(not shown)" : "null"));
53+
54+
try {
55+
FirebaseApp app = FirebaseApp.getInstance(appName);
56+
57+
if ("debug".equals(providerName)) {
58+
59+
// the debug configuration may have a token, or may not
60+
if (debugToken != null) {
61+
// Create a debug provider using hidden factory constructor and our debug token
62+
Class<DebugAppCheckProviderFactory> debugACFactoryClass =
63+
DebugAppCheckProviderFactory.class;
64+
Class<?>[] argType = {String.class};
65+
Constructor c = debugACFactoryClass.getDeclaredConstructor(argType);
66+
Object[] cArgs = {debugToken};
67+
delegateProvider = ((AppCheckProviderFactory) c.newInstance(cArgs)).create(app);
68+
} else {
69+
delegateProvider = DebugAppCheckProviderFactory.getInstance().create(app);
70+
}
71+
}
72+
73+
if ("safetyNet".equals(providerName)) {
74+
delegateProvider = SafetyNetAppCheckProviderFactory.getInstance().create(app);
75+
}
76+
77+
if ("playIntegrity".equals(providerName)) {
78+
delegateProvider = PlayIntegrityAppCheckProviderFactory.getInstance().create(app);
79+
}
80+
} catch (Exception e) {
81+
// This will bubble up and result in a rejected promise with the underlying message
82+
throw new RuntimeException(e.getMessage());
83+
}
84+
}
85+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.invertase.firebase.appcheck;
2+
3+
/*
4+
* Copyright (c) 2023-present Invertase Limited & Contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this library except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
import android.util.Log;
21+
import com.google.firebase.FirebaseApp;
22+
import com.google.firebase.appcheck.AppCheckProvider;
23+
import com.google.firebase.appcheck.AppCheckProviderFactory;
24+
import java.util.HashMap;
25+
import java.util.Map;
26+
27+
public class ReactNativeFirebaseAppCheckProviderFactory implements AppCheckProviderFactory {
28+
private static final String LOGTAG = "RNFBAppCheck";
29+
30+
// This object has one job - create + maintain control over one provider per app
31+
public Map<String, ReactNativeFirebaseAppCheckProvider> providers = new HashMap();
32+
33+
// Our provider will serve as a facade to all the supported native providers,
34+
// we will just pass through configuration calls to it
35+
public void configure(String appName, String providerName, String debugToken) {
36+
Log.d(
37+
LOGTAG,
38+
"ProviderFactory::configure - appName/providerName/debugToken: "
39+
+ appName
40+
+ "/"
41+
+ providerName
42+
+ (debugToken != null ? "/(not shown)" : "/null"));
43+
44+
ReactNativeFirebaseAppCheckProvider provider = null;
45+
46+
// Look up the correct provider for the given appName, create it if not created
47+
provider = providers.get(appName);
48+
if (provider == null) {
49+
provider = new ReactNativeFirebaseAppCheckProvider();
50+
providers.put(appName, provider);
51+
}
52+
provider.configure(appName, providerName, debugToken);
53+
}
54+
55+
public AppCheckProvider create(FirebaseApp firebaseApp) {
56+
String appName = firebaseApp.getName();
57+
Log.d(LOGTAG, "ProviderFactory::create - fetching provider for app " + appName);
58+
ReactNativeFirebaseAppCheckProvider provider = providers.get(appName);
59+
if (provider == null) {
60+
Log.d(LOGTAG, "ProviderFactory::create - provider not configured for this app.");
61+
throw new RuntimeException(
62+
"ReactNativeFirebaseAppCheckProvider not configured for app " + appName);
63+
}
64+
return provider;
65+
}
66+
}

0 commit comments

Comments
 (0)