instances =
+ new ConcurrentHashMap<>();
+
+ @Inject
+ ProviderMultiResourceComponent(RecaptchaEnterpriseAppCheckProviderFactory providerFactory) {
+ this.providerFactory = providerFactory;
+ }
+
+ @NonNull
+ public RecaptchaEnterpriseAppCheckProvider get(@NonNull String siteKey) {
+ RecaptchaEnterpriseAppCheckProvider provider = instances.get(siteKey);
+ if (provider == null) {
+ synchronized (instances) {
+ provider = instances.get(siteKey);
+ if (provider == null) {
+ provider = providerFactory.create(siteKey);
+ instances.put(siteKey, provider);
+ }
+ }
+ }
+ return provider;
+ }
+
+ @AssistedFactory
+ interface RecaptchaEnterpriseAppCheckProviderFactory {
+ RecaptchaEnterpriseAppCheckProvider create(@Assisted String siteKey);
+ }
+}
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java
new file mode 100644
index 00000000000..961ab7defa8
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java
@@ -0,0 +1,142 @@
+// 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 android.app.Application;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+import com.google.android.recaptcha.Recaptcha;
+import com.google.android.recaptcha.RecaptchaAction;
+import com.google.android.recaptcha.RecaptchaTasksClient;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.annotations.concurrent.Blocking;
+import com.google.firebase.annotations.concurrent.Lightweight;
+import com.google.firebase.appcheck.AppCheckProvider;
+import com.google.firebase.appcheck.AppCheckToken;
+import com.google.firebase.appcheck.internal.DefaultAppCheckToken;
+import com.google.firebase.appcheck.internal.NetworkClient;
+import com.google.firebase.appcheck.internal.RetryManager;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedInject;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link AppCheckProvider} that uses reCAPTCHA Enterprise for device
+ * attestation.
+ *
+ * This class orchestrates the flow:
+ *
+ *
+ * - Obtain a reCAPTCHA token via {@code RecaptchaTasksClient}.
+ *
- Exchange the reCAPTCHA token with the Firebase App Check backend to receive a Firebase App
+ * Check token.
+ *
+ */
+public class RecaptchaEnterpriseAppCheckProvider implements AppCheckProvider {
+
+ private final RecaptchaAction recaptchaAction = RecaptchaAction.custom("fire_app_check");
+ private volatile Task recaptchaTasksClientTask;
+ private final Executor liteExecutor;
+ private final Executor blockingExecutor;
+ private final RetryManager retryManager;
+ private final NetworkClient networkClient;
+ private String siteKey;
+ private Application application;
+ private static final String TAG = "rCEAppCheckProvider";
+
+ @AssistedInject
+ public RecaptchaEnterpriseAppCheckProvider(
+ @NonNull FirebaseApp firebaseApp,
+ @Assisted @NonNull String siteKey,
+ @Lightweight Executor liteExecutor,
+ @Blocking Executor blockingExecutor) {
+ this.application = (Application) firebaseApp.getApplicationContext();
+ this.siteKey = siteKey;
+ this.liteExecutor = liteExecutor;
+ this.blockingExecutor = blockingExecutor;
+ this.retryManager = new RetryManager();
+ this.networkClient = new NetworkClient(firebaseApp);
+ }
+
+ @VisibleForTesting
+ RecaptchaEnterpriseAppCheckProvider(
+ @Lightweight Executor liteExecutor,
+ @Blocking Executor blockingExecutor,
+ @NonNull RetryManager retryManager,
+ @NonNull NetworkClient networkClient,
+ @NonNull RecaptchaTasksClient recaptchaTasksClient) {
+ this.liteExecutor = liteExecutor;
+ this.blockingExecutor = blockingExecutor;
+ this.retryManager = retryManager;
+ this.networkClient = networkClient;
+ this.recaptchaTasksClientTask = Tasks.forResult(recaptchaTasksClient);
+ }
+
+ public void initializeRecaptchaClient() {
+ if (recaptchaTasksClientTask == null) {
+ synchronized (this) {
+ if (recaptchaTasksClientTask == null) {
+ Log.d(TAG, "Initializing RecaptchaTasksClient for siteKey: " + siteKey);
+ recaptchaTasksClientTask = Recaptcha.fetchTaskClient(application, siteKey);
+ }
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Task getToken() {
+ return getRecaptchaEnterpriseAttestation()
+ .onSuccessTask(
+ liteExecutor,
+ recaptchaEnterpriseToken -> {
+ ExchangeRecaptchaEnterpriseTokenRequest request =
+ new ExchangeRecaptchaEnterpriseTokenRequest(recaptchaEnterpriseToken);
+ return Tasks.call(
+ blockingExecutor,
+ () ->
+ networkClient.exchangeAttestationForAppCheckToken(
+ request.toJsonString().getBytes(StandardCharsets.UTF_8),
+ NetworkClient.RECAPTCHA_ENTERPRISE,
+ retryManager));
+ })
+ .onSuccessTask(
+ liteExecutor,
+ appCheckTokenResponse ->
+ Tasks.forResult(
+ DefaultAppCheckToken.constructFromAppCheckTokenResponse(
+ appCheckTokenResponse)));
+ }
+
+ @NonNull
+ private Task getRecaptchaEnterpriseAttestation() {
+ return recaptchaTasksClientTask.continueWithTask(
+ blockingExecutor,
+ task -> {
+ if (task.isSuccessful()) {
+ RecaptchaTasksClient client = task.getResult();
+ return client.executeTask(recaptchaAction);
+ } else {
+ Log.w(TAG, "Recaptcha task failed", task.getException());
+ return Tasks.forException((Objects.requireNonNull(task.getException())));
+ }
+ });
+ }
+}
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java
new file mode 100644
index 00000000000..baae7a935b7
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java
@@ -0,0 +1,16 @@
+// 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.
+
+/** @hide */
+package com.google.firebase.appcheck.recaptchaenterprise.internal;
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java
new file mode 100644
index 00000000000..1d3ddadca85
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java
@@ -0,0 +1,38 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.firebase.components.Component;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link FirebaseAppCheckRecaptchaEnterpriseRegistrar}. */
+@RunWith(RobolectricTestRunner.class)
+public class FirebaseAppCheckRecaptchaEnterpriseRegistrarTest {
+ @Test
+ public void testGetComponents() {
+ FirebaseAppCheckRecaptchaEnterpriseRegistrar registrar =
+ new FirebaseAppCheckRecaptchaEnterpriseRegistrar();
+ List> components = registrar.getComponents();
+ assertThat(components).isNotEmpty();
+ assertThat(components).hasSize(2);
+ Component> firebaseExecutorsComponent = components.get(0);
+ assertThat(firebaseExecutorsComponent.isLazy()).isTrue();
+ }
+}
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java
new file mode 100644
index 00000000000..865b69e409c
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java
@@ -0,0 +1,84 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.appcheck.AppCheckProvider;
+import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent;
+import com.google.firebase.appcheck.recaptchaenterprise.internal.RecaptchaEnterpriseAppCheckProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/** Tests for {@link RecaptchaEnterpriseAppCheckProviderFactory}. */
+@RunWith(MockitoJUnitRunner.class)
+public class RecaptchaEnterpriseAppCheckProviderFactoryTest {
+ static final String SITE_KEY_1 = "siteKey1";
+
+ @Mock private FirebaseApp mockFirebaseApp;
+ @Mock private ProviderMultiResourceComponent mockComponent;
+ @Mock private RecaptchaEnterpriseAppCheckProvider mockProvider;
+
+ @Before
+ public void setUp() {
+ when(mockFirebaseApp.get(eq(ProviderMultiResourceComponent.class))).thenReturn(mockComponent);
+ when(mockComponent.get(anyString())).thenReturn(mockProvider);
+ }
+
+ @Test
+ public void getInstance_nonNullSiteKey_returnsNonNullInstance() {
+ RecaptchaEnterpriseAppCheckProviderFactory factory =
+ RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1);
+ assertNotNull(factory);
+ }
+
+ @Test
+ public void getInstance_nullSiteKey_expectThrows() {
+ assertThrows(
+ NullPointerException.class,
+ () -> RecaptchaEnterpriseAppCheckProviderFactory.getInstance(null));
+ }
+
+ @Test
+ public void create_nonNullFirebaseApp_returnsRecaptchaEnterpriseAppCheckProvider() {
+ RecaptchaEnterpriseAppCheckProviderFactory factory =
+ RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1);
+ AppCheckProvider provider = factory.create(mockFirebaseApp);
+ assertNotNull(provider);
+ assertEquals(RecaptchaEnterpriseAppCheckProvider.class, provider.getClass());
+ }
+
+ @Test
+ public void create_callMultipleTimes_providerIsInitializedOnlyOnce() {
+ RecaptchaEnterpriseAppCheckProviderFactory factory =
+ RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1);
+
+ factory.create(mockFirebaseApp);
+ factory.create(mockFirebaseApp);
+ factory.create(mockFirebaseApp);
+ verify(mockProvider, times(1)).initializeRecaptchaClient();
+ }
+}
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java
new file mode 100644
index 00000000000..20e48b7eb46
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java
@@ -0,0 +1,39 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import org.json.JSONObject;
+import org.junit.Test;
+
+/** Tests for {@link ExchangeRecaptchaEnterpriseTokenRequest}. */
+public class ExchangeRecaptchaEnterpriseTokenRequestTest {
+ private static final String RECAPTCHA_ENTERPRISE_TOKEN = "recaptchaEnterpriseToken";
+
+ @Test
+ public void toJsonString_expectSerialized() throws Exception {
+ ExchangeRecaptchaEnterpriseTokenRequest exchangeRecaptchaEnterpriseTokenRequest =
+ new ExchangeRecaptchaEnterpriseTokenRequest(RECAPTCHA_ENTERPRISE_TOKEN);
+
+ String jsonString = exchangeRecaptchaEnterpriseTokenRequest.toJsonString();
+ JSONObject jsonObject = new JSONObject(jsonString);
+
+ assertThat(
+ jsonObject.getString(
+ ExchangeRecaptchaEnterpriseTokenRequest.RECAPTCHA_ENTERPRISE_TOKEN_KEY))
+ .isEqualTo(RECAPTCHA_ENTERPRISE_TOKEN);
+ }
+}
diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java
new file mode 100644
index 00000000000..19cbdc4957b
--- /dev/null
+++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java
@@ -0,0 +1,164 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+import com.google.android.recaptcha.RecaptchaAction;
+import com.google.android.recaptcha.RecaptchaTasksClient;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseException;
+import com.google.firebase.appcheck.AppCheckToken;
+import com.google.firebase.appcheck.internal.AppCheckTokenResponse;
+import com.google.firebase.appcheck.internal.DefaultAppCheckToken;
+import com.google.firebase.appcheck.internal.NetworkClient;
+import com.google.firebase.appcheck.internal.RetryManager;
+import com.google.firebase.concurrent.TestOnlyExecutors;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
+
+/** Tests for {@link RecaptchaEnterpriseAppCheckProvider}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+@LooperMode(LooperMode.Mode.LEGACY)
+public class RecaptchaEnterpriseAppCheckProviderTest {
+ private static final String APP_CHECK_TOKEN = "appCheckToken";
+ private static final String RECAPTCHA_ENTERPRISE_TOKEN = "recaptchaEnterpriseToken";
+ private final Executor liteExecutor = MoreExecutors.directExecutor();
+ private final Executor blockingExecutor = MoreExecutors.directExecutor();
+ private final String siteKey = "siteKey";
+
+ @Mock private NetworkClient mockNetworkClient;
+ @Mock FirebaseApp mockFirebaseApp;
+ @Mock RecaptchaTasksClient mockRecaptchaTasksClient;
+ @Mock RetryManager mockRetryManager;
+
+ @Captor private ArgumentCaptor recaptchaActionCaptor;
+ @Captor private ArgumentCaptor requestCaptor;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublicConstructor_nullFirebaseApp_expectThrows() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new RecaptchaEnterpriseAppCheckProvider(
+ null, siteKey, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking()));
+ }
+
+ @Test
+ public void testPublicConstructor_nullSiteKey_expectThrows() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new RecaptchaEnterpriseAppCheckProvider(
+ mockFirebaseApp, null, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking()));
+ }
+
+ @Test
+ public void getToken_onSuccess_setsTaskResult() throws Exception {
+ when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class)))
+ .thenReturn(Tasks.forResult(RECAPTCHA_ENTERPRISE_TOKEN));
+ String jsonResponse =
+ new JSONObject().put("token", APP_CHECK_TOKEN).put("ttl", 3600).toString();
+ when(mockNetworkClient.exchangeAttestationForAppCheckToken(
+ any(byte[].class), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager)))
+ .thenReturn(AppCheckTokenResponse.fromJsonString(jsonResponse));
+
+ RecaptchaEnterpriseAppCheckProvider provider =
+ new RecaptchaEnterpriseAppCheckProvider(
+ liteExecutor,
+ blockingExecutor,
+ mockRetryManager,
+ mockNetworkClient,
+ mockRecaptchaTasksClient);
+ Task task = provider.getToken();
+
+ assertThat(task.isSuccessful()).isTrue();
+ AppCheckToken token = task.getResult();
+ assertThat(token).isInstanceOf(DefaultAppCheckToken.class);
+ assertThat(token.getToken()).isEqualTo(APP_CHECK_TOKEN);
+
+ verify(mockRecaptchaTasksClient).executeTask(recaptchaActionCaptor.capture());
+ assertThat(recaptchaActionCaptor.getValue().getAction()).isEqualTo("fire_app_check");
+ verify(mockNetworkClient)
+ .exchangeAttestationForAppCheckToken(
+ requestCaptor.capture(), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager));
+ }
+
+ @Test
+ public void getToken_recaptchaFails_returnException() {
+ Exception exception = new Exception("Recaptcha error");
+ when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class)))
+ .thenReturn(Tasks.forException(exception));
+
+ RecaptchaEnterpriseAppCheckProvider provider =
+ new RecaptchaEnterpriseAppCheckProvider(
+ liteExecutor,
+ blockingExecutor,
+ mockRetryManager,
+ mockNetworkClient,
+ mockRecaptchaTasksClient);
+ Task task = provider.getToken();
+ assertThat(task.isSuccessful()).isFalse();
+ assertThat(task.getException()).isEqualTo(exception);
+ }
+
+ @Test
+ public void getToken_networkFails_returnException()
+ throws FirebaseException, JSONException, IOException {
+ when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class)))
+ .thenReturn(Tasks.forResult(RECAPTCHA_ENTERPRISE_TOKEN));
+ Exception exception = new IOException("Network error");
+ when(mockNetworkClient.exchangeAttestationForAppCheckToken(
+ any(byte[].class), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager)))
+ .thenThrow(exception);
+
+ RecaptchaEnterpriseAppCheckProvider provider =
+ new RecaptchaEnterpriseAppCheckProvider(
+ liteExecutor,
+ blockingExecutor,
+ mockRetryManager,
+ mockNetworkClient,
+ mockRecaptchaTasksClient);
+ Task task = provider.getToken();
+ assertThat(task.isSuccessful()).isFalse();
+ assertThat(task.getException()).isEqualTo(exception);
+ }
+}
diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java
index 410f59290cb..fa91e6f8323 100644
--- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java
+++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java
@@ -41,6 +41,7 @@
import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import org.json.JSONException;
/**
@@ -57,9 +58,10 @@ public class NetworkClient {
"https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s";
private static final String PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE =
"https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:generatePlayIntegrityChallenge?key=%s";
+ private static final String RECAPTCHA_ENTERPRISE_URL_TEMPLATE =
+ "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeRecaptchaEnterpriseToken?key=%s";
private static final String CONTENT_TYPE = "Content-Type";
private static final String APPLICATION_JSON = "application/json";
- private static final String UTF_8 = "UTF-8";
@VisibleForTesting static final String X_FIREBASE_CLIENT = "X-Firebase-Client";
@VisibleForTesting static final String X_ANDROID_PACKAGE = "X-Android-Package";
@VisibleForTesting static final String X_ANDROID_CERT = "X-Android-Cert";
@@ -71,12 +73,13 @@ public class NetworkClient {
private final Provider heartBeatControllerProvider;
@Retention(RetentionPolicy.SOURCE)
- @IntDef({UNKNOWN, DEBUG, PLAY_INTEGRITY})
+ @IntDef({UNKNOWN, DEBUG, PLAY_INTEGRITY, RECAPTCHA_ENTERPRISE})
public @interface AttestationTokenType {}
public static final int UNKNOWN = 0;
public static final int DEBUG = 2;
public static final int PLAY_INTEGRITY = 3;
+ public static final int RECAPTCHA_ENTERPRISE = 4;
public NetworkClient(@NonNull FirebaseApp firebaseApp) {
this(
@@ -172,7 +175,8 @@ private String makeNetworkRequest(
? urlConnection.getInputStream()
: urlConnection.getErrorStream();
StringBuilder response = new StringBuilder();
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
@@ -236,6 +240,8 @@ private static String getUrlTemplate(@AttestationTokenType int tokenType) {
return DEBUG_EXCHANGE_URL_TEMPLATE;
case PLAY_INTEGRITY:
return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE;
+ case RECAPTCHA_ENTERPRISE:
+ return RECAPTCHA_ENTERPRISE_URL_TEMPLATE;
default:
throw new IllegalArgumentException("Unknown token type.");
}
@@ -246,7 +252,7 @@ HttpURLConnection createHttpUrlConnection(URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}
- private static final boolean isResponseSuccess(int responseCode) {
+ private static boolean isResponseSuccess(int responseCode) {
return responseCode >= 200 && responseCode < 300;
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 03725a05989..7e3fd691d6a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,8 +4,8 @@
# needs to match the version of grpc that grpc-kotlin has a transitive dependency on.
android-lint = "31.3.2"
androidGradlePlugin = "8.6.1"
-androidx-test-core="1.5.0"
-androidx-test-junit="1.1.5"
+androidx-test-core = "1.5.0"
+androidx-test-junit = "1.1.5"
androidx-test-truth = "1.6.0"
appcompat = "1.7.1"
autoValueParcel = "0.2.6"
@@ -128,7 +128,7 @@ grpc-protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grp
grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
grpc-protoc-gen-kotlin = { module = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpcKotlin" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
-grpc-testing = { module= "io.grpc:grpc-testing", version.ref="grpc" }
+grpc-testing = { module = "io.grpc:grpc-testing", version.ref = "grpc" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
hamcrest-junit = { module = "org.hamcrest:hamcrest-junit", version.ref = "hamcrestJunit" }
hamcrest-library = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrestLibrary" }
@@ -238,4 +238,4 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" }
errorprone = { id = "net.ltgt.errorprone", version.ref = "gradleErrorpronePlugin" }
google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" }
+crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" }
\ No newline at end of file
diff --git a/subprojects.cfg b/subprojects.cfg
index a43cf350e91..e42fd66e518 100644
--- a/subprojects.cfg
+++ b/subprojects.cfg
@@ -3,6 +3,7 @@ appcheck:firebase-appcheck-debug-testing
appcheck:firebase-appcheck-debug
appcheck:firebase-appcheck-interop
appcheck:firebase-appcheck-playintegrity
+appcheck:firebase-appcheck-recaptchaenterprise
appcheck:firebase-appcheck
firebase-abt