diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle
new file mode 100644
index 00000000000..0733ab44ace
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle
@@ -0,0 +1,56 @@
+// Copyright 2022 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 {
+ publishSources = true
+}
+
+android {
+ adbOptions {
+ timeOutInMs 60 * 1000
+ }
+
+ compileSdkVersion project.targetSdkVersion
+ 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 {
+ implementation project(':firebase-common')
+ implementation project(':firebase-components')
+ implementation project(':appcheck:firebase-appcheck')
+ implementation 'com.google.android.gms:play-services-base:18.0.1'
+ implementation 'com.google.android.gms:play-services-tasks:18.0.1'
+ implementation 'com.google.android.play:integrity:1.0.1'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-core:3.4.6'
+ testImplementation "com.google.truth:truth:$googleTruthVersion"
+ testImplementation "org.robolectric:robolectric:$robolectricVersion"
+ testImplementation 'androidx.test:core:1.4.0'
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/gradle.properties b/appcheck/firebase-appcheck-playintegrity/gradle.properties
new file mode 100644
index 00000000000..33424bcee59
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/gradle.properties
@@ -0,0 +1 @@
+version=16.0.0
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..3599f8e6755
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java
new file mode 100644
index 00000000000..743092d2892
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java
@@ -0,0 +1,46 @@
+// Copyright 2022 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.playintegrity;
+
+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.playintegrity.internal.PlayIntegrityAppCheckProvider;
+
+/**
+ * Implementation of an {@link AppCheckProviderFactory} that builds {@link
+ * PlayIntegrityAppCheckProvider}s. This is the default implementation.
+ */
+public class PlayIntegrityAppCheckProviderFactory implements AppCheckProviderFactory {
+
+ private static final PlayIntegrityAppCheckProviderFactory instance =
+ new PlayIntegrityAppCheckProviderFactory();
+
+ /**
+ * Gets an instance of this class for installation into a {@link
+ * com.google.firebase.appcheck.FirebaseAppCheck} instance.
+ */
+ @NonNull
+ public static PlayIntegrityAppCheckProviderFactory getInstance() {
+ return instance;
+ }
+
+ @NonNull
+ @Override
+ public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
+ return new PlayIntegrityAppCheckProvider(firebaseApp);
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java
new file mode 100644
index 00000000000..cc2e68d3054
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java
@@ -0,0 +1,43 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Client-side model of the ExchangePlayIntegrityTokenRequest payload from the Firebase App Check
+ * Token Exchange API.
+ */
+class ExchangePlayIntegrityTokenRequest {
+
+ @VisibleForTesting static final String PLAY_INTEGRITY_TOKEN_KEY = "playIntegrityToken";
+
+ private final String playIntegrityToken;
+
+ public ExchangePlayIntegrityTokenRequest(@NonNull String playIntegrityToken) {
+ this.playIntegrityToken = playIntegrityToken;
+ }
+
+ @NonNull
+ public String toJsonString() throws JSONException {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(PLAY_INTEGRITY_TOKEN_KEY, playIntegrityToken);
+
+ return jsonObject.toString();
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java
new file mode 100644
index 00000000000..5e213d13189
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java
@@ -0,0 +1,35 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import androidx.annotation.NonNull;
+import org.json.JSONObject;
+
+/**
+ * Client-side model of the GeneratePlayIntegrityChallengeRequest payload from the Firebase App
+ * Check Token Exchange API.
+ */
+class GeneratePlayIntegrityChallengeRequest {
+
+ public GeneratePlayIntegrityChallengeRequest() {}
+
+ @NonNull
+ public String toJsonString() {
+ JSONObject jsonObject = new JSONObject();
+
+ // GeneratePlayIntegrityChallenge takes an empty POST body since the app ID is in the URL.
+ return jsonObject.toString();
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java
new file mode 100644
index 00000000000..52d91827276
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java
@@ -0,0 +1,67 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import static com.google.android.gms.common.internal.Preconditions.checkNotNull;
+import static com.google.android.gms.common.util.Strings.emptyToNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.google.firebase.FirebaseException;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Client-side model of the GeneratePlayIntegrityChallengeResponse payload from the Firebase App
+ * Check Token Exchange API.
+ */
+class GeneratePlayIntegrityChallengeResponse {
+
+ @VisibleForTesting static final String CHALLENGE_KEY = "challenge";
+ @VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl";
+
+ private String challenge;
+ private String timeToLive;
+
+ @NonNull
+ public static GeneratePlayIntegrityChallengeResponse fromJsonString(@NonNull String jsonString)
+ throws FirebaseException, JSONException {
+ JSONObject jsonObject = new JSONObject(jsonString);
+ String challenge = emptyToNull(jsonObject.optString(CHALLENGE_KEY));
+ String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY));
+ if (challenge == null || timeToLive == null) {
+ throw new FirebaseException("Unexpected server response.");
+ }
+ return new GeneratePlayIntegrityChallengeResponse(challenge, timeToLive);
+ }
+
+ private GeneratePlayIntegrityChallengeResponse(
+ @NonNull String challenge, @NonNull String timeToLive) {
+ checkNotNull(challenge);
+ checkNotNull(timeToLive);
+ this.challenge = challenge;
+ this.timeToLive = timeToLive;
+ }
+
+ @NonNull
+ public String getChallenge() {
+ return challenge;
+ }
+
+ @NonNull
+ public String getTimeToLive() {
+ return timeToLive;
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java
new file mode 100644
index 00000000000..13381af83a6
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java
@@ -0,0 +1,110 @@
+// Copyright 2022 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.playintegrity.internal;
+
+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.play.core.integrity.IntegrityManager;
+import com.google.android.play.core.integrity.IntegrityManagerFactory;
+import com.google.android.play.core.integrity.IntegrityTokenRequest;
+import com.google.android.play.core.integrity.IntegrityTokenResponse;
+import com.google.firebase.FirebaseApp;
+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 java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class PlayIntegrityAppCheckProvider implements AppCheckProvider {
+
+ private static final String UTF_8 = "UTF-8";
+
+ private final String projectNumber;
+ private final IntegrityManager integrityManager;
+ private final NetworkClient networkClient;
+ private final ExecutorService backgroundExecutor;
+ private final RetryManager retryManager;
+
+ public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) {
+ this(
+ firebaseApp.getOptions().getGcmSenderId(),
+ IntegrityManagerFactory.create(firebaseApp.getApplicationContext()),
+ new NetworkClient(firebaseApp),
+ Executors.newCachedThreadPool(),
+ new RetryManager());
+ }
+
+ @VisibleForTesting
+ PlayIntegrityAppCheckProvider(
+ @NonNull String projectNumber,
+ @NonNull IntegrityManager integrityManager,
+ @NonNull NetworkClient networkClient,
+ @NonNull ExecutorService backgroundExecutor,
+ @NonNull RetryManager retryManager) {
+ this.projectNumber = projectNumber;
+ this.integrityManager = integrityManager;
+ this.networkClient = networkClient;
+ this.backgroundExecutor = backgroundExecutor;
+ this.retryManager = retryManager;
+ }
+
+ @NonNull
+ @Override
+ public Task getToken() {
+ return getPlayIntegrityAttestation()
+ .onSuccessTask(
+ integrityTokenResponse -> {
+ ExchangePlayIntegrityTokenRequest request =
+ new ExchangePlayIntegrityTokenRequest(integrityTokenResponse.token());
+ return Tasks.call(
+ backgroundExecutor,
+ () ->
+ networkClient.exchangeAttestationForAppCheckToken(
+ request.toJsonString().getBytes(UTF_8),
+ NetworkClient.PLAY_INTEGRITY,
+ retryManager));
+ })
+ .onSuccessTask(
+ appCheckTokenResponse -> {
+ return Tasks.forResult(
+ DefaultAppCheckToken.constructFromAppCheckTokenResponse(appCheckTokenResponse));
+ });
+ }
+
+ @NonNull
+ private Task getPlayIntegrityAttestation() {
+ GeneratePlayIntegrityChallengeRequest generateChallengeRequest =
+ new GeneratePlayIntegrityChallengeRequest();
+ Task generateChallengeTask =
+ Tasks.call(
+ backgroundExecutor,
+ () ->
+ GeneratePlayIntegrityChallengeResponse.fromJsonString(
+ networkClient.generatePlayIntegrityChallenge(
+ generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager)));
+ return generateChallengeTask.onSuccessTask(
+ generatePlayIntegrityChallengeResponse -> {
+ return integrityManager.requestIntegrityToken(
+ IntegrityTokenRequest.builder()
+ .setCloudProjectNumber(Long.parseLong(projectNumber))
+ .setNonce(generatePlayIntegrityChallengeResponse.getChallenge())
+ .build());
+ });
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java
new file mode 100644
index 00000000000..3431597172a
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/package-info.java
@@ -0,0 +1,16 @@
+// Copyright 2022 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.playintegrity.internal;
diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java
new file mode 100644
index 00000000000..ed1ebddda96
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java
@@ -0,0 +1,37 @@
+// Copyright 2022 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.playintegrity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link PlayIntegrityAppCheckProviderFactory}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class PlayIntegrityAppCheckProviderFactoryTest {
+
+ @Test
+ public void testGetInstance_callTwice_sameInstance() {
+ PlayIntegrityAppCheckProviderFactory firstInstance =
+ PlayIntegrityAppCheckProviderFactory.getInstance();
+ PlayIntegrityAppCheckProviderFactory secondInstance =
+ PlayIntegrityAppCheckProviderFactory.getInstance();
+ assertThat(firstInstance).isEqualTo(secondInstance);
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java
new file mode 100644
index 00000000000..8c01f97f887
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java
@@ -0,0 +1,42 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ExchangePlayIntegrityTokenRequest}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class ExchangePlayIntegrityTokenRequestTest {
+ private static final String PLAY_INTEGRITY_TOKEN = "playIntegrityToken";
+
+ @Test
+ public void toJsonString_expectSerialized() throws Exception {
+ ExchangePlayIntegrityTokenRequest exchangePlayIntegrityTokenRequest =
+ new ExchangePlayIntegrityTokenRequest(PLAY_INTEGRITY_TOKEN);
+
+ String jsonString = exchangePlayIntegrityTokenRequest.toJsonString();
+ JSONObject jsonObject = new JSONObject(jsonString);
+
+ assertThat(jsonObject.getString(ExchangePlayIntegrityTokenRequest.PLAY_INTEGRITY_TOKEN_KEY))
+ .isEqualTo(PLAY_INTEGRITY_TOKEN);
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java
new file mode 100644
index 00000000000..abd86ae8258
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java
@@ -0,0 +1,37 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link GeneratePlayIntegrityChallengeRequest}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class GeneratePlayIntegrityChallengeRequestTest {
+ private static final String EMPTY_JSON = "{}";
+
+ @Test
+ public void toJsonString_expectSerialized() throws Exception {
+ GeneratePlayIntegrityChallengeRequest generatePlayIntegrityChallengeRequest =
+ new GeneratePlayIntegrityChallengeRequest();
+
+ assertThat(generatePlayIntegrityChallengeRequest.toJsonString()).isEqualTo(EMPTY_JSON);
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java
new file mode 100644
index 00000000000..dcc48034b00
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java
@@ -0,0 +1,65 @@
+// Copyright 2022 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.playintegrity.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.firebase.FirebaseException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link GeneratePlayIntegrityChallengeResponse}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class GeneratePlayIntegrityChallengeResponseTest {
+ private static final String CHALLENGE = "testChallenge";
+ private static final String TIME_TO_LIVE = "3600s";
+
+ @Test
+ public void fromJsonString_expectDeserialized() throws Exception {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE);
+ jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE);
+
+ GeneratePlayIntegrityChallengeResponse generatePlayIntegrityChallengeResponse =
+ GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString());
+ assertThat(generatePlayIntegrityChallengeResponse.getChallenge()).isEqualTo(CHALLENGE);
+ assertThat(generatePlayIntegrityChallengeResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE);
+ }
+
+ @Test
+ public void fromJsonString_nullChallenge_throwsException() throws Exception {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE);
+
+ assertThrows(
+ FirebaseException.class,
+ () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString()));
+ }
+
+ @Test
+ public void fromJsonString_nullTimeToLive_throwsException() throws Exception {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE);
+
+ assertThrows(
+ FirebaseException.class,
+ () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString()));
+ }
+}
diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java
new file mode 100644
index 00000000000..1cbcd153bb3
--- /dev/null
+++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java
@@ -0,0 +1,226 @@
+// Copyright 2022 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.playintegrity.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.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+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.play.core.integrity.IntegrityManager;
+import com.google.android.play.core.integrity.IntegrityTokenRequest;
+import com.google.android.play.core.integrity.IntegrityTokenResponse;
+import com.google.common.util.concurrent.MoreExecutors;
+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 java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeoutException;
+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;
+
+/** Tests for {@link PlayIntegrityAppCheckProvider}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class PlayIntegrityAppCheckProviderTest {
+
+ private static final String PROJECT_NUMBER = "123456";
+ private static final String APP_CHECK_TOKEN = "appCheckToken";
+ private static final String TIME_TO_LIVE = "3600s";
+ private static final String CHALLENGE = "testChallenge";
+ private static final String INTEGRITY_TOKEN = "integrityToken";
+
+ @Mock private IntegrityManager mockIntegrityManager;
+ @Mock private NetworkClient mockNetworkClient;
+ @Mock private RetryManager mockRetryManager;
+ @Mock private IntegrityTokenResponse mockIntegrityTokenResponse;
+ @Mock private AppCheckTokenResponse mockAppCheckTokenResponse;
+
+ @Captor private ArgumentCaptor integrityTokenRequestCaptor;
+ @Captor private ArgumentCaptor exchangePlayIntegrityTokenRequestCaptor;
+
+ private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService();
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mockIntegrityTokenResponse.token()).thenReturn(INTEGRITY_TOKEN);
+ when(mockAppCheckTokenResponse.getToken()).thenReturn(APP_CHECK_TOKEN);
+ when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE);
+ }
+
+ @Test
+ public void testPublicConstructor_nullFirebaseApp_expectThrows() {
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ new PlayIntegrityAppCheckProvider(null);
+ });
+ }
+
+ @Test
+ public void getToken_onSuccess_setsTaskResult() throws Exception {
+ when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager)))
+ .thenReturn(createGeneratePlayIntegrityChallengeResponse());
+ when(mockIntegrityManager.requestIntegrityToken(any()))
+ .thenReturn(Tasks.forResult(mockIntegrityTokenResponse));
+ when(mockNetworkClient.exchangeAttestationForAppCheckToken(
+ any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)))
+ .thenReturn(mockAppCheckTokenResponse);
+
+ PlayIntegrityAppCheckProvider provider =
+ new PlayIntegrityAppCheckProvider(
+ PROJECT_NUMBER,
+ mockIntegrityManager,
+ mockNetworkClient,
+ backgroundExecutor,
+ mockRetryManager);
+ Task task = provider.getToken();
+
+ AppCheckToken token = task.getResult();
+ assertThat(token).isInstanceOf(DefaultAppCheckToken.class);
+ assertThat(token.getToken()).isEqualTo(APP_CHECK_TOKEN);
+
+ verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager));
+
+ verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture());
+ assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber())
+ .isEqualTo(Long.parseLong(PROJECT_NUMBER));
+ assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE);
+
+ verify(mockNetworkClient)
+ .exchangeAttestationForAppCheckToken(
+ exchangePlayIntegrityTokenRequestCaptor.capture(),
+ eq(NetworkClient.PLAY_INTEGRITY),
+ eq(mockRetryManager));
+ String exchangePlayIntegrityTokenRequestJsonString =
+ new String(exchangePlayIntegrityTokenRequestCaptor.getValue());
+ assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN);
+ }
+
+ @Test
+ public void getToken_generateChallengeFails_setsTaskException() throws Exception {
+ when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager)))
+ .thenThrow(new IOException());
+
+ PlayIntegrityAppCheckProvider provider =
+ new PlayIntegrityAppCheckProvider(
+ PROJECT_NUMBER,
+ mockIntegrityManager,
+ mockNetworkClient,
+ backgroundExecutor,
+ mockRetryManager);
+ Task task = provider.getToken();
+
+ assertThat(task.isSuccessful()).isFalse();
+ assertThat(task.getException()).isInstanceOf(IOException.class);
+
+ verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager));
+ verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any());
+ verify(mockIntegrityManager, never()).requestIntegrityToken(any());
+ }
+
+ @Test
+ public void getToken_requestIntegrityTokenFails_setsTaskException() throws Exception {
+ when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager)))
+ .thenReturn(createGeneratePlayIntegrityChallengeResponse());
+ when(mockIntegrityManager.requestIntegrityToken(any()))
+ .thenReturn(Tasks.forException(new TimeoutException()));
+
+ PlayIntegrityAppCheckProvider provider =
+ new PlayIntegrityAppCheckProvider(
+ PROJECT_NUMBER,
+ mockIntegrityManager,
+ mockNetworkClient,
+ backgroundExecutor,
+ mockRetryManager);
+ Task task = provider.getToken();
+
+ assertThat(task.isSuccessful()).isFalse();
+ assertThat(task.getException()).isInstanceOf(TimeoutException.class);
+
+ verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager));
+ verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any());
+
+ verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture());
+ assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber())
+ .isEqualTo(Long.parseLong(PROJECT_NUMBER));
+ assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE);
+ }
+
+ @Test
+ public void getToken_tokenExchangeFails_setsTaskException() throws Exception {
+ when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager)))
+ .thenReturn(createGeneratePlayIntegrityChallengeResponse());
+ when(mockIntegrityManager.requestIntegrityToken(any()))
+ .thenReturn(Tasks.forResult(mockIntegrityTokenResponse));
+ when(mockNetworkClient.exchangeAttestationForAppCheckToken(
+ any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)))
+ .thenThrow(new IOException());
+
+ PlayIntegrityAppCheckProvider provider =
+ new PlayIntegrityAppCheckProvider(
+ PROJECT_NUMBER,
+ mockIntegrityManager,
+ mockNetworkClient,
+ backgroundExecutor,
+ mockRetryManager);
+ Task task = provider.getToken();
+
+ assertThat(task.isSuccessful()).isFalse();
+ assertThat(task.getException()).isInstanceOf(IOException.class);
+
+ verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager));
+
+ verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture());
+ assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber())
+ .isEqualTo(Long.parseLong(PROJECT_NUMBER));
+ assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE);
+
+ verify(mockNetworkClient)
+ .exchangeAttestationForAppCheckToken(
+ exchangePlayIntegrityTokenRequestCaptor.capture(),
+ eq(NetworkClient.PLAY_INTEGRITY),
+ eq(mockRetryManager));
+ String exchangePlayIntegrityTokenRequestJsonString =
+ new String(exchangePlayIntegrityTokenRequestCaptor.getValue());
+ assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN);
+ }
+
+ private static String createGeneratePlayIntegrityChallengeResponse() throws Exception {
+ JSONObject responseBodyJson = new JSONObject();
+ responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE);
+ responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE);
+
+ return responseBodyJson.toString();
+ }
+}
diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java
index 77469b0dbfe..407ad603c3d 100644
--- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java
+++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/AppCheckTokenResponse.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.google.firebase.FirebaseException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -35,10 +36,13 @@ public class AppCheckTokenResponse {
@NonNull
public static AppCheckTokenResponse fromJsonString(@NonNull String jsonString)
- throws JSONException {
+ throws FirebaseException, JSONException {
JSONObject jsonObject = new JSONObject(jsonString);
String token = emptyToNull(jsonObject.optString(TOKEN_KEY));
String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY));
+ if (token == null || timeToLive == null) {
+ throw new FirebaseException("Unexpected server response.");
+ }
return new AppCheckTokenResponse(token, timeToLive);
}
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 ffa012288e6..7317524dfc2 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
@@ -55,6 +55,10 @@ public class NetworkClient {
"https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeSafetyNetToken?key=%s";
private static final String DEBUG_EXCHANGE_URL_TEMPLATE =
"https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeDebugToken?key=%s";
+ private static final String PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE =
+ "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 CONTENT_TYPE = "Content-Type";
private static final String APPLICATION_JSON = "application/json";
private static final String UTF_8 = "UTF-8";
@@ -69,12 +73,13 @@ public class NetworkClient {
private final Provider heartBeatControllerProvider;
@Retention(RetentionPolicy.SOURCE)
- @IntDef({UNKNOWN, SAFETY_NET, DEBUG})
+ @IntDef({UNKNOWN, SAFETY_NET, DEBUG, PLAY_INTEGRITY})
public @interface AttestationTokenType {}
public static final int UNKNOWN = 0;
public static final int SAFETY_NET = 1;
public static final int DEBUG = 2;
+ public static final int PLAY_INTEGRITY = 3;
public NetworkClient(@NonNull FirebaseApp firebaseApp) {
this(
@@ -116,6 +121,29 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken(
throw new FirebaseException("Too many attempts.");
}
URL url = new URL(String.format(getUrlTemplate(tokenType), projectId, appId, apiKey));
+ String response = makeNetworkRequest(url, requestBytes, retryManager);
+ return AppCheckTokenResponse.fromJsonString(response);
+ }
+
+ /**
+ * Calls the App Check backend using {@link HttpURLConnection} in order to generate a challenge
+ * nonce for the Play Integrity attestation flow.
+ */
+ @NonNull
+ public String generatePlayIntegrityChallenge(
+ @NonNull byte[] requestBytes, @NonNull RetryManager retryManager)
+ throws FirebaseException, IOException, JSONException {
+ if (!retryManager.canRetry()) {
+ throw new FirebaseException("Too many attempts.");
+ }
+ URL url =
+ new URL(String.format(PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE, projectId, appId, apiKey));
+ return makeNetworkRequest(url, requestBytes, retryManager);
+ }
+
+ private String makeNetworkRequest(
+ @NonNull URL url, @NonNull byte[] requestBytes, @NonNull RetryManager retryManager)
+ throws FirebaseException, IOException, JSONException {
HttpURLConnection urlConnection = createHttpUrlConnection(url);
try {
@@ -160,7 +188,7 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken(
+ httpErrorResponse.getErrorMessage());
}
retryManager.resetBackoffOnSuccess();
- return AppCheckTokenResponse.fromJsonString(responseBody);
+ return responseBody;
} finally {
urlConnection.disconnect();
}
@@ -203,6 +231,8 @@ private static String getUrlTemplate(@AttestationTokenType int tokenType) {
return SAFETY_NET_EXCHANGE_URL_TEMPLATE;
case DEBUG:
return DEBUG_EXCHANGE_URL_TEMPLATE;
+ case PLAY_INTEGRITY:
+ return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE;
default:
throw new IllegalArgumentException("Unknown token type.");
}
diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java
index 63e48cced87..db2255d151f 100644
--- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java
+++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/AppCheckTokenResponseTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import com.google.firebase.FirebaseException;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -49,8 +50,7 @@ public void fromJsonString_nullToken_throwsException() throws Exception {
jsonObject.put(AppCheckTokenResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE);
assertThrows(
- NullPointerException.class,
- () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString()));
+ FirebaseException.class, () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString()));
}
@Test
@@ -59,7 +59,6 @@ public void fromJsonString_nullTimeToLive_throwsException() throws Exception {
jsonObject.put(AppCheckTokenResponse.TOKEN_KEY, APP_CHECK_TOKEN);
assertThrows(
- NullPointerException.class,
- () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString()));
+ FirebaseException.class, () -> AppCheckTokenResponse.fromJsonString(jsonObject.toString()));
}
}
diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java
index 44d920b4cd6..9a2441f97b6 100644
--- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java
+++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java
@@ -69,6 +69,10 @@ public class NetworkClientTest {
"https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangeSafetyNetToken?key=apiKey";
private static final String DEBUG_EXPECTED_URL =
"https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangeDebugToken?key=apiKey";
+ private static final String PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL =
+ "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:generatePlayIntegrityChallenge?key=apiKey";
+ private static final String PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL =
+ "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangePlayIntegrityToken?key=apiKey";
private static final String JSON_REQUEST = "jsonRequest";
private static final int SUCCESS_CODE = 200;
private static final int ERROR_CODE = 404;
@@ -76,6 +80,7 @@ public class NetworkClientTest {
private static final String TIME_TO_LIVE = "3600s";
private static final String ERROR_MESSAGE = "error message";
private static final String HEART_BEAT_HEADER_TEST = "test-header";
+ private static final String CHALLENGE_RESPONSE = "challengeResponse";
@Mock HeartBeatController mockHeartBeatController;
@Mock HttpURLConnection mockHttpUrlConnection;
@@ -211,17 +216,53 @@ public void exchangeDebugToken_errorResponse_throwsException() throws Exception
}
@Test
- public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Exception {
+ public void exchangePlayIntegrityToken_successResponse_returnsAppCheckTokenResponse()
+ throws Exception {
JSONObject responseBodyJson = createAttestationResponse();
when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
when(mockHttpUrlConnection.getInputStream())
.thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes()));
when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE);
- // The heartbeat request header should not be attached when the heartbeat is HeartBeat.NONE.
- networkClient.exchangeAttestationForAppCheckToken(
- JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager);
+ AppCheckTokenResponse tokenResponse =
+ networkClient.exchangeAttestationForAppCheckToken(
+ JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager);
+ assertThat(tokenResponse.getToken()).isEqualTo(APP_CHECK_TOKEN);
+ assertThat(tokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE);
+
+ URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL);
+ verify(networkClient).createHttpUrlConnection(expectedUrl);
+ verify(mockOutputStream)
+ .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length);
+ verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt());
+ verify(mockRetryManager).resetBackoffOnSuccess();
+ verifyRequestHeaders();
+ }
+
+ @Test
+ public void exchangePlayIntegrityToken_errorResponse_throwsException() throws Exception {
+ JSONObject responseBodyJson = createHttpErrorResponse();
+
+ when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
+ when(mockHttpUrlConnection.getErrorStream())
+ .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes()));
+ when(mockHttpUrlConnection.getResponseCode()).thenReturn(ERROR_CODE);
+
+ FirebaseException exception =
+ assertThrows(
+ FirebaseException.class,
+ () ->
+ networkClient.exchangeAttestationForAppCheckToken(
+ JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager));
+
+ assertThat(exception.getMessage()).contains(ERROR_MESSAGE);
+ URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL);
+ verify(networkClient).createHttpUrlConnection(expectedUrl);
+ verify(mockOutputStream)
+ .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length);
+ verify(mockRetryManager).updateBackoffOnFailure(ERROR_CODE);
+ verify(mockRetryManager, never()).resetBackoffOnSuccess();
verifyRequestHeaders();
}
@@ -237,6 +278,21 @@ public void exchangeUnknownAttestation_throwsException() {
verify(mockRetryManager, never()).resetBackoffOnSuccess();
}
+ @Test
+ public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Exception {
+ JSONObject responseBodyJson = createAttestationResponse();
+
+ when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
+ when(mockHttpUrlConnection.getInputStream())
+ .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes()));
+ when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE);
+ // The heartbeat request header should not be attached when the heartbeat is HeartBeat.NONE.
+ networkClient.exchangeAttestationForAppCheckToken(
+ JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager);
+
+ verifyRequestHeaders();
+ }
+
@Test
public void exchangeAttestation_cannotRetry_throwsException() {
when(mockRetryManager.canRetry()).thenReturn(false);
@@ -253,6 +309,68 @@ public void exchangeAttestation_cannotRetry_throwsException() {
verify(mockRetryManager, never()).resetBackoffOnSuccess();
}
+ @Test
+ public void generatePlayIntegrityChallenge_successResponse_returnsJsonString() throws Exception {
+ when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
+ when(mockHttpUrlConnection.getInputStream())
+ .thenReturn(new ByteArrayInputStream(CHALLENGE_RESPONSE.getBytes()));
+ when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE);
+
+ String challengeResponse =
+ networkClient.generatePlayIntegrityChallenge(JSON_REQUEST.getBytes(), mockRetryManager);
+ assertThat(challengeResponse).isEqualTo(CHALLENGE_RESPONSE);
+
+ URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL);
+ verify(networkClient).createHttpUrlConnection(expectedUrl);
+ verify(mockOutputStream)
+ .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length);
+ verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt());
+ verify(mockRetryManager).resetBackoffOnSuccess();
+ verifyRequestHeaders();
+ }
+
+ @Test
+ public void generatePlayIntegrityChallenge_errorResponse_throwsException() throws Exception {
+ JSONObject responseBodyJson = createHttpErrorResponse();
+
+ when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
+ when(mockHttpUrlConnection.getErrorStream())
+ .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes()));
+ when(mockHttpUrlConnection.getResponseCode()).thenReturn(ERROR_CODE);
+
+ FirebaseException exception =
+ assertThrows(
+ FirebaseException.class,
+ () ->
+ networkClient.generatePlayIntegrityChallenge(
+ JSON_REQUEST.getBytes(), mockRetryManager));
+
+ assertThat(exception.getMessage()).contains(ERROR_MESSAGE);
+ URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL);
+ verify(networkClient).createHttpUrlConnection(expectedUrl);
+ verify(mockOutputStream)
+ .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length);
+ verify(mockRetryManager).updateBackoffOnFailure(ERROR_CODE);
+ verify(mockRetryManager, never()).resetBackoffOnSuccess();
+ verifyRequestHeaders();
+ }
+
+ @Test
+ public void generatePlayIntegrityChallenge_cannotRetry_throwsException() {
+ when(mockRetryManager.canRetry()).thenReturn(false);
+
+ FirebaseException exception =
+ assertThrows(
+ FirebaseException.class,
+ () ->
+ networkClient.generatePlayIntegrityChallenge(
+ JSON_REQUEST.getBytes(), mockRetryManager));
+
+ assertThat(exception.getMessage()).contains("Too many attempts");
+ verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt());
+ verify(mockRetryManager, never()).resetBackoffOnSuccess();
+ }
+
private void verifyRequestHeaders() {
verify(networkClient).getHeartBeat();
verify(mockHttpUrlConnection).setRequestProperty(X_FIREBASE_CLIENT, HEART_BEAT_HEADER_TEST);
diff --git a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java
index 41299c2276d..38eca67b561 100644
--- a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java
+++ b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java
@@ -30,6 +30,7 @@
import com.google.firebase.appcheck.FirebaseAppCheck;
import com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener;
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
+import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory;
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory;
import com.google.firebase.storage.FirebaseStorage;
import com.google.firebase.storage.ListResult;
@@ -42,6 +43,7 @@ public class MainActivity extends AppCompatActivity {
private FirebaseAppCheck firebaseAppCheck;
private FirebaseStorage firebaseStorage;
private AppCheckListener appCheckListener;
+ private Button installPlayIntegrityButton;
private Button installSafetyNetButton;
private Button installDebugButton;
private Button getAppCheckTokenButton;
@@ -80,6 +82,7 @@ public void onAppCheckTokenChanged(@NonNull AppCheckToken token) {
}
private void initViews() {
+ installPlayIntegrityButton = findViewById(R.id.install_play_integrity_app_check_button);
installSafetyNetButton = findViewById(R.id.install_safety_net_app_check_button);
installDebugButton = findViewById(R.id.install_debug_app_check_button);
getAppCheckTokenButton = findViewById(R.id.exchange_app_check_button);
@@ -89,6 +92,17 @@ private void initViews() {
}
private void setOnClickListeners() {
+ installPlayIntegrityButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ firebaseAppCheck.installAppCheckProviderFactory(
+ PlayIntegrityAppCheckProviderFactory.getInstance());
+ Log.d(TAG, "Installed PlayIntegrityAppCheckProvider");
+ showToast("Installed PlayIntegrityAppCheckProvider.");
+ }
+ });
+
installSafetyNetButton.setOnClickListener(
new OnClickListener() {
@Override
diff --git a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml
index eccdbf9ec6e..43a4d771cb7 100644
--- a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml
+++ b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml
@@ -6,6 +6,11 @@
android:gravity="center"
android:orientation="vertical">
+