Skip to content

Commit c5b712f

Browse files
feat(appcheck): Implement reCAPTCHA Enterprise App Check provider (#7125)
This PR introduces the new `firebase-appcheck-recaptchaenterprise` module, enabling reCAPTCHA Enterprise as an attestation provider for Firebase App Check. **Key Changes & Components:** * **`RecaptchaEnterpriseAppCheckProviderFactory.java`**: Factory for creating reCAPTCHA Enterprise App Check providers, abstracting the reCAPTCHA client initialization. * **`FirebaseAppCheckRecaptchaEnterpriseRegistrar.java`**: Handles automatic registration of the provider via Firebase's component system. * **`SiteKey.java`**: A utility class to encapsulate the reCAPTCHA Enterprise site key. * **`ExchangeRecaptchaEnterpriseTokenRequest.java`**: Defines the client-side data model for the App Check Token Exchange API payload. * **`NetworkClient.java`**: Extended to include the specific endpoint and token type for reCAPTCHA Enterprise attestation exchange. * **`RecaptchaEnterpriseAppCheckProvider.java`**: The core `AppCheckProvider` implementation, responsible for: * Interacting with the reCAPTCHA Android client to obtain attestation tokens. * Using the `NetworkClient` to exchange these tokens with the Firebase App Check backend. * Converting the backend response into a usable `AppCheckToken`. * **Build System Integration**: * `AndroidManifest.xml`: Updated with necessary internet permissions for the reCAPTCHA Android SDK. * `firebase-appcheck-recaptchaenterprise.gradle`: New module's build configuration, including dependencies. * `subprojects.cfg`: Module added to the Firebase Android SDK's multi-project build.
1 parent 44428b9 commit c5b712f

18 files changed

+886
-8
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Signature format: 3.0
2+
package com.google.firebase.appcheck.recaptchaenterprise {
3+
4+
public class RecaptchaEnterpriseAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory {
5+
method public com.google.firebase.appcheck.AppCheckProvider create(com.google.firebase.FirebaseApp);
6+
method public static com.google.firebase.appcheck.recaptchaenterprise.RecaptchaEnterpriseAppCheckProviderFactory getInstance(String);
7+
}
8+
9+
}
10+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Signature format: 3.0
2+
package com.google.firebase.appcheck.recaptchaenterprise {
3+
4+
public class RecaptchaEnterpriseAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory {
5+
method public com.google.firebase.appcheck.AppCheckProvider create(com.google.firebase.FirebaseApp);
6+
method public static com.google.firebase.appcheck.recaptchaenterprise.RecaptchaEnterpriseAppCheckProviderFactory getInstance(String);
7+
}
8+
9+
}
10+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
plugins {
16+
id 'firebase-library'
17+
}
18+
19+
firebaseLibrary {
20+
libraryGroup = "appcheck"
21+
releaseNotes {
22+
name.set("{{app_check}} Recaptcha Enterprise")
23+
versionName.set("appcheck-recaptchaenterprise")
24+
}
25+
}
26+
27+
android {
28+
adbOptions {
29+
timeOutInMs 60 * 1000
30+
}
31+
32+
namespace "com.google.firebase.appcheck.recaptchaenterprise"
33+
compileSdkVersion project.compileSdkVersion
34+
defaultConfig {
35+
targetSdkVersion project.targetSdkVersion
36+
minSdkVersion project.minSdkVersion
37+
versionName version
38+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
39+
}
40+
compileOptions {
41+
sourceCompatibility JavaVersion.VERSION_1_8
42+
targetCompatibility JavaVersion.VERSION_1_8
43+
}
44+
45+
testOptions.unitTests.includeAndroidResources = false
46+
}
47+
48+
dependencies {
49+
implementation(libs.dagger.dagger)
50+
51+
api project(':appcheck:firebase-appcheck')
52+
api 'com.google.firebase:firebase-common'
53+
api 'com.google.firebase:firebase-components'
54+
api 'com.google.android.recaptcha:recaptcha:18.7.1'
55+
56+
annotationProcessor(libs.dagger.compiler)
57+
58+
testImplementation(project(":integ-testing")) {
59+
exclude group: 'com.google.firebase', module: 'firebase-common'
60+
exclude group: 'com.google.firebase', module: 'firebase-components'
61+
}
62+
testImplementation libs.androidx.test.core
63+
testImplementation libs.truth
64+
testImplementation libs.junit
65+
testImplementation libs.mockito.core
66+
testImplementation libs.robolectric
67+
testImplementation libs.org.json
68+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?><!-- Copyright 2025 Google LLC -->
2+
<!-- -->
3+
<!-- Licensed under the Apache License, Version 2.0 (the "License"); -->
4+
<!-- you may not use this file except in compliance with the License. -->
5+
<!-- You may obtain a copy of the License at -->
6+
<!-- -->
7+
<!-- http://www.apache.org/licenses/LICENSE-2.0 -->
8+
<!-- -->
9+
<!-- Unless required by applicable law or agreed to in writing, software -->
10+
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
11+
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
12+
<!-- See the License for the specific language governing permissions and -->
13+
<!-- limitations under the License. -->
14+
15+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
16+
17+
<application>
18+
<service
19+
android:name="com.google.firebase.components.ComponentDiscoveryService"
20+
android:exported="false">
21+
<meta-data
22+
android:name="com.google.firebase.components:com.google.firebase.appcheck.recaptchaenterprise.FirebaseAppCheckRecaptchaEnterpriseRegistrar"
23+
android:value="com.google.firebase.components.ComponentRegistrar" />
24+
</service>
25+
</application>
26+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.recaptchaenterprise;
16+
17+
import com.google.android.gms.common.annotation.KeepForSdk;
18+
import com.google.firebase.FirebaseApp;
19+
import com.google.firebase.annotations.concurrent.Blocking;
20+
import com.google.firebase.annotations.concurrent.Lightweight;
21+
import com.google.firebase.appcheck.recaptchaenterprise.internal.DaggerProviderComponent;
22+
import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent;
23+
import com.google.firebase.components.Component;
24+
import com.google.firebase.components.ComponentRegistrar;
25+
import com.google.firebase.components.Dependency;
26+
import com.google.firebase.components.Qualified;
27+
import com.google.firebase.platforminfo.LibraryVersionComponent;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.concurrent.Executor;
31+
32+
/**
33+
* {@link ComponentRegistrar} for setting up FirebaseAppCheck reCAPTCHA Enterprise's dependency
34+
* injections in Firebase Android Components.
35+
*
36+
* @hide
37+
*/
38+
@KeepForSdk
39+
public class FirebaseAppCheckRecaptchaEnterpriseRegistrar implements ComponentRegistrar {
40+
private static final String LIBRARY_NAME = "fire-app-check-recaptcha-enterprise";
41+
42+
@Override
43+
public List<Component<?>> getComponents() {
44+
Qualified<Executor> liteExecutor = Qualified.qualified(Lightweight.class, Executor.class);
45+
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class);
46+
47+
return Arrays.asList(
48+
Component.builder(ProviderMultiResourceComponent.class)
49+
.name(LIBRARY_NAME)
50+
.add(Dependency.required(FirebaseApp.class))
51+
.add(Dependency.required(liteExecutor))
52+
.add(Dependency.required(blockingExecutor))
53+
.factory(
54+
container ->
55+
DaggerProviderComponent.builder()
56+
.setFirebaseApp(container.get(FirebaseApp.class))
57+
.setLiteExecutor(container.get(liteExecutor))
58+
.setBlockingExecutor(container.get(blockingExecutor))
59+
.build()
60+
.getMultiResourceComponent())
61+
.build(),
62+
LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME));
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.recaptchaenterprise;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.firebase.FirebaseApp;
19+
import com.google.firebase.appcheck.AppCheckProvider;
20+
import com.google.firebase.appcheck.AppCheckProviderFactory;
21+
import com.google.firebase.appcheck.FirebaseAppCheck;
22+
import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent;
23+
import com.google.firebase.appcheck.recaptchaenterprise.internal.RecaptchaEnterpriseAppCheckProvider;
24+
import java.util.Objects;
25+
26+
/**
27+
* Implementation of an {@link AppCheckProviderFactory} that builds <br>
28+
* {@link RecaptchaEnterpriseAppCheckProvider}s. This is the default implementation.
29+
*/
30+
public class RecaptchaEnterpriseAppCheckProviderFactory implements AppCheckProviderFactory {
31+
32+
private final String siteKey;
33+
private volatile RecaptchaEnterpriseAppCheckProvider provider;
34+
35+
private RecaptchaEnterpriseAppCheckProviderFactory(@NonNull String siteKey) {
36+
this.siteKey = siteKey;
37+
}
38+
39+
/** Gets an instance of this class for installation into a {@link FirebaseAppCheck} instance. */
40+
@NonNull
41+
public static RecaptchaEnterpriseAppCheckProviderFactory getInstance(@NonNull String siteKey) {
42+
Objects.requireNonNull(siteKey, "siteKey cannot be null");
43+
return new RecaptchaEnterpriseAppCheckProviderFactory(siteKey);
44+
}
45+
46+
@NonNull
47+
@Override
48+
@SuppressWarnings("FirebaseUseExplicitDependencies")
49+
public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
50+
if (provider == null) {
51+
synchronized (this) {
52+
if (provider == null) {
53+
ProviderMultiResourceComponent component =
54+
firebaseApp.get(ProviderMultiResourceComponent.class);
55+
provider = component.get(siteKey);
56+
provider.initializeRecaptchaClient();
57+
}
58+
}
59+
}
60+
return provider;
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.recaptchaenterprise.internal;
16+
17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.VisibleForTesting;
19+
import org.json.JSONException;
20+
import org.json.JSONObject;
21+
22+
/**
23+
* Client-side model of the ExchangeRecaptchaEnterpriseTokenRequest payload from the Firebase App
24+
* Check Token Exchange API.
25+
*/
26+
public class ExchangeRecaptchaEnterpriseTokenRequest {
27+
28+
@VisibleForTesting
29+
static final String RECAPTCHA_ENTERPRISE_TOKEN_KEY = "recaptchaEnterpriseToken";
30+
31+
private final String recaptchaEnterpriseToken;
32+
33+
public ExchangeRecaptchaEnterpriseTokenRequest(@NonNull String recaptchaEnterpriseToken) {
34+
this.recaptchaEnterpriseToken = recaptchaEnterpriseToken;
35+
}
36+
37+
@NonNull
38+
public String toJsonString() throws JSONException {
39+
JSONObject jsonObject = new JSONObject();
40+
jsonObject.put(RECAPTCHA_ENTERPRISE_TOKEN_KEY, recaptchaEnterpriseToken);
41+
42+
return jsonObject.toString();
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.recaptchaenterprise.internal;
16+
17+
import com.google.firebase.FirebaseApp;
18+
import com.google.firebase.annotations.concurrent.Blocking;
19+
import com.google.firebase.annotations.concurrent.Lightweight;
20+
import dagger.BindsInstance;
21+
import dagger.Component;
22+
import dagger.Module;
23+
import java.util.concurrent.Executor;
24+
import javax.inject.Singleton;
25+
26+
@Singleton
27+
@Component(modules = ProviderComponent.MainModule.class)
28+
public interface ProviderComponent {
29+
ProviderMultiResourceComponent getMultiResourceComponent();
30+
31+
@Component.Builder
32+
interface Builder {
33+
@BindsInstance
34+
Builder setFirebaseApp(FirebaseApp firebaseApp);
35+
36+
@BindsInstance
37+
Builder setLiteExecutor(@Lightweight Executor liteExecutor);
38+
39+
@BindsInstance
40+
Builder setBlockingExecutor(@Blocking Executor blockingExecutor);
41+
42+
ProviderComponent build();
43+
}
44+
45+
@Module
46+
abstract class MainModule {}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.recaptchaenterprise.internal;
16+
17+
import androidx.annotation.NonNull;
18+
import dagger.assisted.Assisted;
19+
import dagger.assisted.AssistedFactory;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import javax.inject.Inject;
23+
import javax.inject.Singleton;
24+
25+
/** Multi-resource container for RecaptchaEnterpriseAppCheckProvider */
26+
@Singleton
27+
public final class ProviderMultiResourceComponent {
28+
private final RecaptchaEnterpriseAppCheckProviderFactory providerFactory;
29+
30+
private final Map<String, RecaptchaEnterpriseAppCheckProvider> instances =
31+
new ConcurrentHashMap<>();
32+
33+
@Inject
34+
ProviderMultiResourceComponent(RecaptchaEnterpriseAppCheckProviderFactory providerFactory) {
35+
this.providerFactory = providerFactory;
36+
}
37+
38+
@NonNull
39+
public RecaptchaEnterpriseAppCheckProvider get(@NonNull String siteKey) {
40+
RecaptchaEnterpriseAppCheckProvider provider = instances.get(siteKey);
41+
if (provider == null) {
42+
synchronized (instances) {
43+
provider = instances.get(siteKey);
44+
if (provider == null) {
45+
provider = providerFactory.create(siteKey);
46+
instances.put(siteKey, provider);
47+
}
48+
}
49+
}
50+
return provider;
51+
}
52+
53+
@AssistedFactory
54+
interface RecaptchaEnterpriseAppCheckProviderFactory {
55+
RecaptchaEnterpriseAppCheckProvider create(@Assisted String siteKey);
56+
}
57+
}

0 commit comments

Comments
 (0)