-
Notifications
You must be signed in to change notification settings - Fork 626
feat(appcheck): Implement reCAPTCHA Enterprise App Check provider #7125
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
base: main
Are you sure you want to change the base?
Changes from all commits
e193c14
5288e7c
c25340b
86c3bbb
68ac173
014dc4a
2d2b96d
7a99c47
c8e295b
5f925b4
9c34a6e
290a9fb
b6dea8d
ff30974
874ef41
f30a68e
24056ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// Signature format: 3.0 | ||
package com.google.firebase.appcheck.recaptchaenterprise { | ||
|
||
public class RecaptchaEnterpriseAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory { | ||
method public com.google.firebase.appcheck.AppCheckProvider create(com.google.firebase.FirebaseApp); | ||
method public static com.google.firebase.appcheck.recaptchaenterprise.RecaptchaEnterpriseAppCheckProviderFactory getInstance(String); | ||
} | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright 2025 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file 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. | ||
|
||
plugins { | ||
id 'firebase-library' | ||
} | ||
|
||
firebaseLibrary { | ||
libraryGroup = "appcheck" | ||
releaseNotes { | ||
name.set("{{app_check}} Recaptcha Enterprise") | ||
versionName.set("appcheck-recaptchaenterprise") | ||
} | ||
} | ||
|
||
android { | ||
adbOptions { | ||
timeOutInMs 60 * 1000 | ||
} | ||
|
||
namespace "com.google.firebase.appcheck.recaptchaenterprise" | ||
compileSdkVersion project.compileSdkVersion | ||
defaultConfig { | ||
targetSdkVersion project.targetSdkVersion | ||
minSdkVersion project.minSdkVersion | ||
versionName version | ||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
compileOptions { | ||
sourceCompatibility JavaVersion.VERSION_1_8 | ||
targetCompatibility JavaVersion.VERSION_1_8 | ||
} | ||
|
||
testOptions.unitTests.includeAndroidResources = false | ||
} | ||
|
||
dependencies { | ||
api project(':appcheck:firebase-appcheck') | ||
api 'com.google.firebase:firebase-common' | ||
api 'com.google.firebase:firebase-components' | ||
api 'com.google.android.recaptcha:recaptcha:18.7.1' | ||
|
||
testImplementation(project(":integ-testing")) { | ||
exclude group: 'com.google.firebase', module: 'firebase-common' | ||
exclude group: 'com.google.firebase', module: 'firebase-components' | ||
} | ||
testImplementation libs.androidx.test.core | ||
testImplementation libs.truth | ||
testImplementation libs.junit | ||
testImplementation libs.mockito.core | ||
testImplementation libs.robolectric | ||
testImplementation libs.org.json | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright 2025 Google LLC --> | ||
<!-- --> | ||
<!-- Licensed under the Apache License, Version 2.0 (the "License"); --> | ||
<!-- you may not use this file 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. --> | ||
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
<application> | ||
<service | ||
android:name="com.google.firebase.components.ComponentDiscoveryService" | ||
android:exported="false"> | ||
<meta-data | ||
android:name="com.google.firebase.components:com.google.firebase.appcheck.recaptchaenterprise.FirebaseAppCheckRecaptchaEnterpriseRegistrar" | ||
android:value="com.google.firebase.components.ComponentRegistrar" /> | ||
</service> | ||
</application> | ||
</manifest> |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,57 @@ | ||||||
// Copyright 2025 Google LLC | ||||||
// | ||||||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
// you may not use this file 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. | ||||||
|
||||||
package com.google.firebase.appcheck.recaptchaenterprise; | ||||||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
import com.google.android.gms.common.annotation.KeepForSdk; | ||||||
import com.google.firebase.annotations.concurrent.Blocking; | ||||||
import com.google.firebase.annotations.concurrent.Lightweight; | ||||||
import com.google.firebase.appcheck.recaptchaenterprise.internal.FirebaseExecutors; | ||||||
import com.google.firebase.components.Component; | ||||||
import com.google.firebase.components.ComponentRegistrar; | ||||||
import com.google.firebase.components.Dependency; | ||||||
import com.google.firebase.components.Qualified; | ||||||
import com.google.firebase.platforminfo.LibraryVersionComponent; | ||||||
import java.util.Arrays; | ||||||
import java.util.List; | ||||||
import java.util.concurrent.Executor; | ||||||
|
||||||
/** | ||||||
* {@link ComponentRegistrar} for setting up FirebaseAppCheck reCAPTCHA Enterprise's dependency | ||||||
* injections in Firebase Android Components. | ||||||
* | ||||||
* @hide | ||||||
*/ | ||||||
@KeepForSdk | ||||||
public class FirebaseAppCheckRecaptchaEnterpriseRegistrar implements ComponentRegistrar { | ||||||
private static final String LIBRARY_NAME = "fire-app-check-recaptcha-enterprise"; | ||||||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
@Override | ||||||
public List<Component<?>> getComponents() { | ||||||
Qualified<Executor> liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); | ||||||
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); | ||||||
|
||||||
return Arrays.asList( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taking a step back, the whole registrar is not doing what it's suppose to do. In Firebase, the components system uses these Line 47 in 5ffc702
The current implementation does not create anything related to this component There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the debug as a good example of how this should be done Line 47 in 5ffc702
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need different objects of RecaptchaEnterpriseAppCheckProvider for each site key but firebaseApp.get(class) returns a singleton instance of the requested class. That's why we are manually creating the object of RecaptchaEnterpriseAppCheckProvider. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PlayIntegrity doesn't use site key, so singleton works there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See functions on how multi resources is managed in the components system |
||||||
Component.builder(FirebaseExecutors.class) | ||||||
.name(LIBRARY_NAME) | ||||||
.add(Dependency.required(liteExecutor)) | ||||||
.add(Dependency.required(blockingExecutor)) | ||||||
.factory( | ||||||
container -> | ||||||
new FirebaseExecutors( | ||||||
container.get(liteExecutor), container.get(blockingExecutor))) | ||||||
.build(), | ||||||
LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Copyright 2025 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file 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. | ||
|
||
package com.google.firebase.appcheck.recaptchaenterprise; | ||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
rlazo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import android.app.Application; | ||
import androidx.annotation.NonNull; | ||
import com.google.firebase.FirebaseApp; | ||
import com.google.firebase.appcheck.AppCheckProvider; | ||
import com.google.firebase.appcheck.AppCheckProviderFactory; | ||
import com.google.firebase.appcheck.FirebaseAppCheck; | ||
import com.google.firebase.appcheck.recaptchaenterprise.internal.FirebaseExecutors; | ||
import com.google.firebase.appcheck.recaptchaenterprise.internal.RecaptchaEnterpriseAppCheckProvider; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
/** | ||
* Implementation of an {@link AppCheckProviderFactory} that builds <br> | ||
* {@link RecaptchaEnterpriseAppCheckProvider}s. This is the default implementation. | ||
*/ | ||
public class RecaptchaEnterpriseAppCheckProviderFactory implements AppCheckProviderFactory { | ||
|
||
private static FirebaseExecutors firebaseExecutors; | ||
private static final Map<String, RecaptchaEnterpriseAppCheckProviderFactory> factoryInstances = | ||
new ConcurrentHashMap<>(); | ||
private final String siteKey; | ||
private volatile RecaptchaEnterpriseAppCheckProvider provider; | ||
|
||
private RecaptchaEnterpriseAppCheckProviderFactory(@NonNull String siteKey) { | ||
this.siteKey = siteKey; | ||
} | ||
|
||
/** Gets an instance of this class for installation into a {@link FirebaseAppCheck} instance. */ | ||
@NonNull | ||
public static RecaptchaEnterpriseAppCheckProviderFactory getInstance(@NonNull String siteKey) { | ||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I you are using a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. putIfAbsent requires API level 24 (current min is 23) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documentation says it's api level 1. Is it wrong? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works but throws lint eror https://github.com/firebase/firebase-android-sdk/actions/runs/16336679413/job/46150167302?pr=7125 |
||
return factoryInstances.computeIfAbsent( | ||
siteKey, RecaptchaEnterpriseAppCheckProviderFactory::new); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
@SuppressWarnings("FirebaseUseExplicitDependencies") | ||
public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { | ||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (provider == null) { | ||
synchronized (this) { | ||
if (provider == null) { | ||
if (RecaptchaEnterpriseAppCheckProviderFactory.firebaseExecutors == null) { | ||
firebaseExecutors = firebaseApp.get(FirebaseExecutors.class); | ||
} | ||
Application application = (Application) firebaseApp.getApplicationContext(); | ||
|
||
provider = | ||
new RecaptchaEnterpriseAppCheckProvider( | ||
firebaseApp, | ||
application, | ||
siteKey, | ||
firebaseExecutors.getLiteExecutor(), | ||
firebaseExecutors.getBlockingExecutor()); | ||
} | ||
} | ||
} | ||
return provider; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2025 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file 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. | ||
|
||
package com.google.firebase.appcheck.recaptchaenterprise.internal; | ||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import androidx.annotation.NonNull; | ||
import androidx.annotation.VisibleForTesting; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
|
||
/** | ||
* Client-side model of the ExchangeRecaptchaEnterpriseTokenRequest payload from the Firebase App | ||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Check Token Exchange API. | ||
*/ | ||
public class ExchangeRecaptchaEnterpriseTokenRequest { | ||
|
||
@VisibleForTesting | ||
static final String RECAPTCHA_ENTERPRISE_TOKEN_KEY = "recaptchaEnterpriseToken"; | ||
|
||
private final String recaptchaEnterpriseToken; | ||
|
||
public ExchangeRecaptchaEnterpriseTokenRequest(@NonNull String recaptchaEnterpriseToken) { | ||
this.recaptchaEnterpriseToken = recaptchaEnterpriseToken; | ||
} | ||
|
||
@NonNull | ||
public String toJsonString() throws JSONException { | ||
JSONObject jsonObject = new JSONObject(); | ||
jsonObject.put(RECAPTCHA_ENTERPRISE_TOKEN_KEY, recaptchaEnterpriseToken); | ||
|
||
return jsonObject.toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,43 @@ | ||||
// Copyright 2025 Google LLC | ||||
// | ||||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||||
// you may not use this file 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. | ||||
|
||||
package com.google.firebase.appcheck.recaptchaenterprise.internal; | ||||
hiteshmaurya56 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
import com.google.firebase.annotations.concurrent.Blocking; | ||||
import com.google.firebase.annotations.concurrent.Lightweight; | ||||
import java.util.concurrent.Executor; | ||||
|
||||
/** | ||||
* This class encapsulates a {@link com.google.firebase.annotations.concurrent.Lightweight} executor | ||||
* and a {@link com.google.firebase.annotations.concurrent.Blocking} executor, making them available | ||||
* for various asynchronous operations related to reCAPTCHA Enterprise App Check. | ||||
*/ | ||||
public class FirebaseExecutors { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need to encapsulate the executors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find a way to get lite and blocking executors in RecaptchaEnterpriseAppCheckProviderFactory (Line 77,78) to create RecaptchaEnterpriseAppCheckProvider as we can't get executors by calling firebaseApp.get(Class). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See how the Debug provider works Line 47 in 5ffc702
It uses the component system correctly to get all those dependencies. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Debug provider doesn't depend on any site key, so singleton works there and can get instance using firebaseApp.getInstance(Class). Recaptcha provider is dependent on site key and different instances are required for each site key. We can't use firebaseApp.get(class), so we are manually creating instances. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the functions multi resource style linked above |
||||
private final Executor liteExecutor; | ||||
private final Executor blockingExecutor; | ||||
|
||||
public FirebaseExecutors( | ||||
@Lightweight Executor liteExecutor, @Blocking Executor blockingExecutor) { | ||||
this.liteExecutor = liteExecutor; | ||||
this.blockingExecutor = blockingExecutor; | ||||
} | ||||
|
||||
public Executor getLiteExecutor() { | ||||
return liteExecutor; | ||||
} | ||||
|
||||
public Executor getBlockingExecutor() { | ||||
return blockingExecutor; | ||||
} | ||||
} |
Uh oh!
There was an error while loading. Please reload this page.