Skip to content

Commit 295c8f5

Browse files
authored
Implement Play Integrity token exchange. (#3613)
* Implement Play Integrity exchange. * Update and add unit tests. * Add @nonnull annotation.
1 parent cf98935 commit 295c8f5

File tree

9 files changed

+350
-11
lines changed

9 files changed

+350
-11
lines changed

appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ dependencies {
4646
implementation project(':appcheck:firebase-appcheck')
4747
implementation 'com.google.android.gms:play-services-base:18.0.1'
4848
implementation 'com.google.android.gms:play-services-tasks:18.0.1'
49+
implementation 'com.google.android.play:integrity:1.0.1'
4950

50-
testImplementation 'junit:junit:4.13-beta-2'
51+
testImplementation 'junit:junit:4.13.2'
52+
testImplementation 'org.mockito:mockito-core:3.4.6'
53+
testImplementation "com.google.truth:truth:$googleTruthVersion"
54+
testImplementation "org.robolectric:robolectric:$robolectricVersion"
55+
testImplementation 'androidx.test:core:1.4.0'
5156
}

appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ public static PlayIntegrityAppCheckProviderFactory getInstance() {
4141
@NonNull
4242
@Override
4343
public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
44-
return new PlayIntegrityAppCheckProvider();
44+
return new PlayIntegrityAppCheckProvider(firebaseApp);
4545
}
4646
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2022 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.playintegrity.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 ExchangePlayIntegrityTokenRequest payload from the Firebase App Check
24+
* Token Exchange API.
25+
*/
26+
public class ExchangePlayIntegrityTokenRequest {
27+
28+
@VisibleForTesting static final String PLAY_INTEGRITY_TOKEN_KEY = "playIntegrityToken";
29+
30+
private final String playIntegrityToken;
31+
32+
public ExchangePlayIntegrityTokenRequest(@NonNull String playIntegrityToken) {
33+
this.playIntegrityToken = playIntegrityToken;
34+
}
35+
36+
@NonNull
37+
public String toJsonString() throws JSONException {
38+
JSONObject jsonObject = new JSONObject();
39+
jsonObject.put(PLAY_INTEGRITY_TOKEN_KEY, playIntegrityToken);
40+
41+
return jsonObject.toString();
42+
}
43+
}

appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,67 @@
1515
package com.google.firebase.appcheck.playintegrity.internal;
1616

1717
import androidx.annotation.NonNull;
18+
import androidx.annotation.VisibleForTesting;
19+
import com.google.android.gms.tasks.Continuation;
1820
import com.google.android.gms.tasks.Task;
1921
import com.google.android.gms.tasks.Tasks;
20-
import com.google.firebase.FirebaseException;
22+
import com.google.firebase.FirebaseApp;
2123
import com.google.firebase.appcheck.AppCheckProvider;
2224
import com.google.firebase.appcheck.AppCheckToken;
25+
import com.google.firebase.appcheck.internal.AppCheckTokenResponse;
26+
import com.google.firebase.appcheck.internal.DefaultAppCheckToken;
27+
import com.google.firebase.appcheck.internal.NetworkClient;
28+
import com.google.firebase.appcheck.internal.RetryManager;
29+
import java.util.concurrent.ExecutorService;
30+
import java.util.concurrent.Executors;
2331

2432
public class PlayIntegrityAppCheckProvider implements AppCheckProvider {
2533

26-
public PlayIntegrityAppCheckProvider() {}
34+
private static final String UTF_8 = "UTF-8";
35+
36+
private final NetworkClient networkClient;
37+
private final ExecutorService backgroundExecutor;
38+
private final RetryManager retryManager;
39+
40+
public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) {
41+
this(new NetworkClient(firebaseApp), Executors.newCachedThreadPool(), new RetryManager());
42+
}
43+
44+
@VisibleForTesting
45+
PlayIntegrityAppCheckProvider(
46+
@NonNull NetworkClient networkClient,
47+
@NonNull ExecutorService backgroundExecutor,
48+
@NonNull RetryManager retryManager) {
49+
this.networkClient = networkClient;
50+
this.backgroundExecutor = backgroundExecutor;
51+
this.retryManager = retryManager;
52+
}
2753

2854
@NonNull
2955
@Override
3056
public Task<AppCheckToken> getToken() {
31-
// TODO(rosalyntan): Implement this.
32-
return Tasks.forException(new FirebaseException("Unimplemented"));
57+
// TODO(rosalyntan): Obtain the Play Integrity challenge nonce.
58+
ExchangePlayIntegrityTokenRequest request =
59+
new ExchangePlayIntegrityTokenRequest("placeholder");
60+
Task<AppCheckTokenResponse> networkTask =
61+
Tasks.call(
62+
backgroundExecutor,
63+
() ->
64+
networkClient.exchangeAttestationForAppCheckToken(
65+
request.toJsonString().getBytes(UTF_8),
66+
NetworkClient.PLAY_INTEGRITY,
67+
retryManager));
68+
return networkTask.continueWithTask(
69+
new Continuation<AppCheckTokenResponse, Task<AppCheckToken>>() {
70+
@Override
71+
public Task<AppCheckToken> then(@NonNull Task<AppCheckTokenResponse> task) {
72+
if (task.isSuccessful()) {
73+
return Tasks.forResult(
74+
DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult()));
75+
}
76+
// TODO: Surface more error details.
77+
return Tasks.forException(task.getException());
78+
}
79+
});
3380
}
3481
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2022 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.playintegrity;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
import org.robolectric.RobolectricTestRunner;
22+
import org.robolectric.annotation.Config;
23+
24+
/** Tests for {@link PlayIntegrityAppCheckProviderFactory}. */
25+
@RunWith(RobolectricTestRunner.class)
26+
@Config(manifest = Config.NONE)
27+
public class PlayIntegrityAppCheckProviderFactoryTest {
28+
29+
@Test
30+
public void testGetInstance_callTwice_sameInstance() {
31+
PlayIntegrityAppCheckProviderFactory firstInstance =
32+
PlayIntegrityAppCheckProviderFactory.getInstance();
33+
PlayIntegrityAppCheckProviderFactory secondInstance =
34+
PlayIntegrityAppCheckProviderFactory.getInstance();
35+
assertThat(firstInstance).isEqualTo(secondInstance);
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2022 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.playintegrity.internal;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
19+
import org.json.JSONObject;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.robolectric.RobolectricTestRunner;
23+
import org.robolectric.annotation.Config;
24+
25+
/** Tests for {@link ExchangePlayIntegrityTokenRequest}. */
26+
@RunWith(RobolectricTestRunner.class)
27+
@Config(manifest = Config.NONE)
28+
public class ExchangePlayIntegrityTokenRequestTest {
29+
private static final String PLAY_INTEGRITY_TOKEN = "playIntegrityToken";
30+
31+
@Test
32+
public void toJsonString_expectSerialized() throws Exception {
33+
ExchangePlayIntegrityTokenRequest exchangePlayIntegrityTokenRequest =
34+
new ExchangePlayIntegrityTokenRequest(PLAY_INTEGRITY_TOKEN);
35+
36+
String jsonString = exchangePlayIntegrityTokenRequest.toJsonString();
37+
JSONObject jsonObject = new JSONObject(jsonString);
38+
39+
assertThat(jsonObject.getString(ExchangePlayIntegrityTokenRequest.PLAY_INTEGRITY_TOKEN_KEY))
40+
.isEqualTo(PLAY_INTEGRITY_TOKEN);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2022 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.playintegrity.internal;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.junit.Assert.assertThrows;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.ArgumentMatchers.eq;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
import com.google.android.gms.tasks.Task;
25+
import com.google.common.util.concurrent.MoreExecutors;
26+
import com.google.firebase.appcheck.AppCheckToken;
27+
import com.google.firebase.appcheck.internal.AppCheckTokenResponse;
28+
import com.google.firebase.appcheck.internal.DefaultAppCheckToken;
29+
import com.google.firebase.appcheck.internal.NetworkClient;
30+
import com.google.firebase.appcheck.internal.RetryManager;
31+
import java.io.IOException;
32+
import java.util.concurrent.ExecutorService;
33+
import org.junit.Before;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
import org.mockito.Mock;
37+
import org.mockito.MockitoAnnotations;
38+
import org.robolectric.RobolectricTestRunner;
39+
import org.robolectric.annotation.Config;
40+
41+
/** Tests for {@link PlayIntegrityAppCheckProvider}. */
42+
@RunWith(RobolectricTestRunner.class)
43+
@Config(manifest = Config.NONE)
44+
public class PlayIntegrityAppCheckProviderTest {
45+
46+
private static final String ATTESTATION_TOKEN = "token";
47+
private static final String TIME_TO_LIVE = "3600s";
48+
49+
private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService();
50+
@Mock private NetworkClient mockNetworkClient;
51+
@Mock private RetryManager mockRetryManager;
52+
@Mock private AppCheckTokenResponse mockAppCheckTokenResponse;
53+
54+
@Before
55+
public void setup() {
56+
MockitoAnnotations.initMocks(this);
57+
}
58+
59+
@Test
60+
public void testPublicConstructor_nullFirebaseApp_expectThrows() {
61+
assertThrows(
62+
NullPointerException.class,
63+
() -> {
64+
new PlayIntegrityAppCheckProvider(null);
65+
});
66+
}
67+
68+
@Test
69+
public void getToken_onSuccess_setsTaskResult() throws Exception {
70+
when(mockNetworkClient.exchangeAttestationForAppCheckToken(
71+
any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)))
72+
.thenReturn(mockAppCheckTokenResponse);
73+
when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN);
74+
when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE);
75+
76+
PlayIntegrityAppCheckProvider provider =
77+
new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager);
78+
Task<AppCheckToken> task = provider.getToken();
79+
80+
verify(mockNetworkClient)
81+
.exchangeAttestationForAppCheckToken(
82+
any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager));
83+
84+
AppCheckToken token = task.getResult();
85+
assertThat(token).isInstanceOf(DefaultAppCheckToken.class);
86+
assertThat(token.getToken()).isEqualTo(ATTESTATION_TOKEN);
87+
}
88+
89+
@Test
90+
public void getToken_onFailure_setsTaskException() throws Exception {
91+
when(mockNetworkClient.exchangeAttestationForAppCheckToken(
92+
any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)))
93+
.thenThrow(new IOException());
94+
95+
PlayIntegrityAppCheckProvider provider =
96+
new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager);
97+
Task<AppCheckToken> task = provider.getToken();
98+
99+
verify(mockNetworkClient)
100+
.exchangeAttestationForAppCheckToken(
101+
any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager));
102+
103+
assertThat(task.isSuccessful()).isFalse();
104+
Exception exception = task.getException();
105+
assertThat(exception).isInstanceOf(IOException.class);
106+
}
107+
}

appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public class NetworkClient {
5555
"https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeSafetyNetToken?key=%s";
5656
private static final String DEBUG_EXCHANGE_URL_TEMPLATE =
5757
"https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeDebugToken?key=%s";
58+
private static final String PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE =
59+
"https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s";
5860
private static final String CONTENT_TYPE = "Content-Type";
5961
private static final String APPLICATION_JSON = "application/json";
6062
private static final String UTF_8 = "UTF-8";
@@ -69,12 +71,13 @@ public class NetworkClient {
6971
private final Provider<HeartBeatController> heartBeatControllerProvider;
7072

7173
@Retention(RetentionPolicy.SOURCE)
72-
@IntDef({UNKNOWN, SAFETY_NET, DEBUG})
74+
@IntDef({UNKNOWN, SAFETY_NET, DEBUG, PLAY_INTEGRITY})
7375
public @interface AttestationTokenType {}
7476

7577
public static final int UNKNOWN = 0;
7678
public static final int SAFETY_NET = 1;
7779
public static final int DEBUG = 2;
80+
public static final int PLAY_INTEGRITY = 3;
7881

7982
public NetworkClient(@NonNull FirebaseApp firebaseApp) {
8083
this(
@@ -203,6 +206,8 @@ private static String getUrlTemplate(@AttestationTokenType int tokenType) {
203206
return SAFETY_NET_EXCHANGE_URL_TEMPLATE;
204207
case DEBUG:
205208
return DEBUG_EXCHANGE_URL_TEMPLATE;
209+
case PLAY_INTEGRITY:
210+
return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE;
206211
default:
207212
throw new IllegalArgumentException("Unknown token type.");
208213
}

0 commit comments

Comments
 (0)