Skip to content

Add headers to support Android-application restricted API keys #2988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ AppDistributionReleaseInternal getNewReleaseFromClient(
throws FirebaseAppDistributionException {
try {
AppDistributionReleaseInternal retrievedNewRelease =
firebaseAppDistributionTesterApiClient.fetchNewRelease(fid, appId, apiKey, authToken);
firebaseAppDistributionTesterApiClient.fetchNewRelease(
fid, appId, apiKey, authToken, firebaseApp.getApplicationContext());

if (isNewerBuildVersion(retrievedNewRelease) || !isInstalledRelease(retrievedNewRelease)) {
return retrievedNewRelease;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@

package com.google.firebase.appdistribution;

import static android.content.ContentValues.TAG;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.NETWORK_FAILURE;

import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gms.common.util.AndroidUtilsLight;
import com.google.android.gms.common.util.Hex;
import com.google.firebase.appdistribution.internal.AppDistributionReleaseInternal;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
Expand All @@ -36,18 +42,27 @@ class FirebaseAppDistributionTesterApiClient {
private static final String REQUEST_METHOD = "GET";
private static final String API_KEY_HEADER = "x-goog-api-key";
private static final String INSTALLATION_AUTH_HEADER = "X-Goog-Firebase-Installations-Auth";
private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package";
private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert";

private static final String BUILD_VERSION_JSON_KEY = "buildVersion";
private static final String DISPLAY_VERSION_JSON_KEY = "displayVersion";
private static final String RELEASE_NOTES_JSON_KEY = "releaseNotes";
private static final String BINARY_TYPE_JSON_KEY = "binaryType";
private static final String CODE_HASH_KEY = "codeHash";
private static final String IAS_ARTIFACT_ID_KEY = "iasArtifactId";
private static final String DOWNLOAD_URL_KEY = "downloadUrl";

private static final String TAG = "TesterApiClient:";

public static final int DEFAULT_BUFFER_SIZE = 8192;

public @NonNull AppDistributionReleaseInternal fetchNewRelease(
@NonNull String fid, @NonNull String appId, @NonNull String apiKey, @NonNull String authToken)
@NonNull String fid,
@NonNull String appId,
@NonNull String apiKey,
@NonNull String authToken,
@NonNull Context context)
throws FirebaseAppDistributionException {

AppDistributionReleaseInternal newRelease;
Expand All @@ -56,6 +71,9 @@ class FirebaseAppDistributionTesterApiClient {
connection.setRequestMethod(REQUEST_METHOD);
connection.setRequestProperty(API_KEY_HEADER, apiKey);
connection.setRequestProperty(INSTALLATION_AUTH_HEADER, authToken);
connection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
connection.addRequestProperty(
X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage(context));

InputStream inputStream = connection.getInputStream();
JSONObject newReleaseJson = readFetchReleaseInputStream(inputStream);
Expand Down Expand Up @@ -185,4 +203,27 @@ private static String convertInputStreamToString(InputStream is) throws IOExcept
}
return result.toString();
}

/**
* Gets the Android package's SHA-1 fingerprint.
*
* @param context
*/
private String getFingerprintHashForPackage(Context context) {
byte[] hash;

try {
hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName());

if (hash == null) {
Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName());
return null;
} else {
return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "No such package: " + context.getPackageName(), e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Bundle;
Expand Down Expand Up @@ -87,6 +88,7 @@ public class CheckForNewReleaseClientTest {

private CheckForNewReleaseClient checkForNewReleaseClient;
private ShadowPackageManager shadowPackageManager;
private Context applicationContext;

@Mock private FirebaseInstallationsApi mockFirebaseInstallations;
@Mock private FirebaseAppDistributionTesterApiClient mockFirebaseAppDistributionTesterApiClient;
Expand Down Expand Up @@ -117,6 +119,7 @@ public void setup() {

shadowPackageManager =
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
applicationContext = ApplicationProvider.getApplicationContext();

ApplicationInfo applicationInfo =
ApplicationInfoBuilder.newBuilder()
Expand Down Expand Up @@ -161,7 +164,8 @@ public void checkForNewReleaseTask_whenCalledMultipleTimes_returnsTheSameTask()

@Test
public void checkForNewRelease_succeeds() throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(any(), any(), any(), any()))
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
any(), any(), any(), any(), any()))
.thenReturn(TEST_RELEASE_NEWER_APK);
when(mockFirebaseInstallations.getId()).thenReturn(Tasks.forResult(TEST_FID_1));
when(mockFirebaseInstallations.getToken(false))
Expand All @@ -180,7 +184,8 @@ public void checkForNewRelease_succeeds() throws Exception {

@Test
public void checkForNewRelease_nonAppDistroFailure() throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(any(), any(), any(), any()))
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
any(), any(), any(), any(), any()))
.thenReturn(TEST_RELEASE_CURRENT);
Exception expectedException = new Exception("test ex");
when(mockFirebaseInstallations.getId()).thenReturn(Tasks.forException(expectedException));
Expand Down Expand Up @@ -210,7 +215,8 @@ public void checkForNewRelease_appDistroFailure() throws Exception {
FirebaseAppDistributionException expectedException =
new FirebaseAppDistributionException(
"test", FirebaseAppDistributionException.Status.UNKNOWN);
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(any(), any(), any(), any()))
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
any(), any(), any(), any(), any()))
.thenThrow(expectedException);

TestOnCompleteListener<AppDistributionReleaseInternal> onCompleteListener =
Expand All @@ -228,7 +234,7 @@ public void checkForNewRelease_appDistroFailure() throws Exception {
public void getNewReleaseFromClient_whenNewReleaseIsNewerBuildThanInstalled_returnsRelease()
throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN))
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext))
.thenReturn(TEST_RELEASE_NEWER_APK);

AppDistributionReleaseInternal release =
Expand All @@ -242,7 +248,7 @@ public void getNewReleaseFromClient_whenNewReleaseIsNewerBuildThanInstalled_retu
@Test
public void getNewReleaseFromClient_whenNewReleaseIsSameRelease_returnsNull() throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN))
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext))
.thenReturn(TEST_RELEASE_CURRENT);

doReturn(TEST_CODEHASH_2).when(checkForNewReleaseClient).extractApkCodeHash(any());
Expand All @@ -256,7 +262,8 @@ public void getNewReleaseFromClient_whenNewReleaseIsSameRelease_returnsNull() th

@Test
public void handleNewReleaseFromClient_whenNewAabIsAvailable_returnsRelease() throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(any(), any(), any(), any()))
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
any(), any(), any(), any(), any()))
.thenReturn(
AppDistributionReleaseInternal.builder()
.setBuildVersion(TEST_RELEASE_CURRENT.getBuildVersion())
Expand Down Expand Up @@ -285,7 +292,8 @@ public void handleNewReleaseFromClient_whenNewAabIsAvailable_returnsRelease() th
@Test
public void handleNewReleaseFromClient_whenNewReleaseIsSameAsInstalledAab_returnsNull()
throws Exception {
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(any(), any(), any(), any()))
when(mockFirebaseAppDistributionTesterApiClient.fetchNewRelease(
any(), any(), any(), any(), any()))
.thenReturn(
AppDistributionReleaseInternal.builder()
.setBuildVersion(TEST_RELEASE_CURRENT.getBuildVersion())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.when;

import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import com.google.firebase.appdistribution.internal.AppDistributionReleaseInternal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand All @@ -45,6 +47,7 @@ public class FirebaseAppDistributionTesterApiClientTest {
private static final String INVALID_RESPONSE = "InvalidResponse";

private FirebaseAppDistributionTesterApiClient firebaseAppDistributionTesterApiClient;
private Context applicationContext;
@Mock private HttpsURLConnection mockHttpsURLConnection;

@Before
Expand All @@ -59,6 +62,8 @@ public void setup() throws Exception {
Mockito.doReturn(mockHttpsURLConnection)
.when(firebaseAppDistributionTesterApiClient)
.openHttpsUrlConnection(TEST_APP_ID_1, TEST_FID_1);

applicationContext = ApplicationProvider.getApplicationContext();
}

@Test
Expand All @@ -69,7 +74,7 @@ public void fetchNewRelease_whenResponseSuccessfulForApk_returnsRelease() throws
when(mockHttpsURLConnection.getInputStream()).thenReturn(response);
AppDistributionReleaseInternal release =
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN);
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext);
assertEquals(release.getBinaryType(), BinaryType.APK);
assertEquals(release.getBuildVersion(), "3");
assertEquals(release.getDisplayVersion(), "3.0");
Expand All @@ -86,7 +91,7 @@ public void fetchNewRelease_whenResponseSuccessfulForAab_returnsRelease() throws
when(mockHttpsURLConnection.getInputStream()).thenReturn(response);
AppDistributionReleaseInternal release =
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN);
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext);
assertEquals(release.getBinaryType(), BinaryType.AAB);
assertEquals(release.getBuildVersion(), "3");
assertEquals(release.getDisplayVersion(), "3.0");
Expand All @@ -105,7 +110,7 @@ public void fetchNewRelease_whenResponseFailsWith401_throwsError() throws Except
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
assertEquals("Failed to authenticate the tester", ex.getMessage());
Expand All @@ -121,7 +126,7 @@ public void fetchNewRelease_whenResponseFailsWith403_throwsError() throws Except
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
assertEquals("Failed to authorize the tester", ex.getMessage());
Expand All @@ -137,7 +142,7 @@ public void fetchNewRelease_whenResponseFailsWith404_throwsError() throws Except
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
assertEquals("Tester or release not found", ex.getMessage());
Expand All @@ -153,7 +158,7 @@ public void fetchNewRelease_whenResponseFailsWith504_throwsError() throws Except
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.NETWORK_FAILURE, ex.getErrorCode());
assertEquals("Failed to fetch releases due to timeout", ex.getMessage());
Expand All @@ -169,7 +174,7 @@ public void fetchNewRelease_whenResponseFailsWithUnknownCode_throwsError() throw
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.NETWORK_FAILURE, ex.getErrorCode());
assertEquals("Failed to fetch releases due to unknown network error", ex.getMessage());
Expand All @@ -185,7 +190,7 @@ public void fetchNewRelease_whenInvalidJson_throwsError() throws Exception {
FirebaseAppDistributionException.class,
() ->
firebaseAppDistributionTesterApiClient.fetchNewRelease(
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN));
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));

assertEquals(FirebaseAppDistributionException.Status.UNKNOWN, ex.getErrorCode());
assertEquals("Error parsing service response", ex.getMessage());
Expand Down