Skip to content

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions appcheck/firebase-appcheck-recaptchaenterprise/api.txt
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;

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";

@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(
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 *Registrars to know how to build a given component when needed. For example, PlayIntegrity registers how to get a Provider,

The current implementation does not create anything related to this component

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Author

@hiteshmaurya56 hiteshmaurya56 Jul 17, 2025

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlayIntegrity doesn't use site key, so singleton works there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

putIfAbsent requires API level 24 (current min is 23)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-07-16 at 2 48 01 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says it's api level 1. Is it wrong?

Copy link
Author

@hiteshmaurya56 hiteshmaurya56 Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return factoryInstances.computeIfAbsent(
siteKey, RecaptchaEnterpriseAppCheckProviderFactory::new);
}

@NonNull
@Override
@SuppressWarnings("FirebaseUseExplicitDependencies")
public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
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;

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
* 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;

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to encapsulate the executors?

Copy link
Author

Choose a reason for hiding this comment

The 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).
Is there any better way to get executors in RecaptchaEnterpriseAppCheckProviderFactory?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how the Debug provider works

It uses the component system correctly to get all those dependencies.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
}
Loading
Loading