diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 1a7f890c6d3..06028ad7b5d 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -17,6 +17,7 @@ package com.google.firebase.installations { } public enum FirebaseInstallationsException.Status { + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status AUTHENTICATION_ERROR; enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status CLIENT_ERROR; enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status SDK_INTERNAL_ERROR; } @@ -81,13 +82,13 @@ package com.google.firebase.installations.remote { ctor public FirebaseInstallationServiceClient(@NonNull Context); method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); - method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); } public abstract class InstallationResponse { ctor public InstallationResponse(); method @NonNull public static com.google.firebase.installations.remote.InstallationResponse.Builder builder(); - method @Nullable public abstract InstallationTokenResult getAuthToken(); + method @Nullable public abstract com.google.firebase.installations.remote.TokenResult getAuthToken(); method @Nullable public abstract String getFid(); method @Nullable public abstract String getRefreshToken(); method @Nullable public abstract com.google.firebase.installations.remote.InstallationResponse.ResponseCode getResponseCode(); @@ -98,7 +99,7 @@ package com.google.firebase.installations.remote { public abstract static class InstallationResponse.Builder { ctor public InstallationResponse.Builder(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse build(); - method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull InstallationTokenResult); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull com.google.firebase.installations.remote.TokenResult); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setFid(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setRefreshToken(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setResponseCode(@NonNull com.google.firebase.installations.remote.InstallationResponse.ResponseCode); @@ -110,5 +111,29 @@ package com.google.firebase.installations.remote { enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode SERVER_ERROR; } + public abstract class TokenResult { + ctor public TokenResult(); + method @NonNull public static com.google.firebase.installations.remote.TokenResult.Builder builder(); + method @Nullable public abstract com.google.firebase.installations.remote.TokenResult.ResponseCode getResponseCode(); + method @Nullable public abstract String getToken(); + method @NonNull public abstract long getTokenExpirationTimestamp(); + method public boolean isSuccessful(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder toBuilder(); + } + + public abstract static class TokenResult.Builder { + ctor public TokenResult.Builder(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult build(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setResponseCode(@NonNull com.google.firebase.installations.remote.TokenResult.ResponseCode); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setToken(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setTokenExpirationTimestamp(long); + } + + public enum TokenResult.ResponseCode { + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode FID_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode OK; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode REFRESH_TOKEN_ERROR; + } + } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index dc569af259c..26ae5acd6c8 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -29,12 +29,12 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_TOKEN_RESULT; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -59,6 +59,7 @@ import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.TokenResult; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -174,7 +175,7 @@ public void setUp() throws FirebaseException { // Mocks successful auth token generation when(backendClientReturnsOk.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_TOKEN_RESULT); + .thenReturn(TEST_TOKEN_RESULT); when(persistedInstallationReturnsError.insertOrUpdatePersistedInstallationEntry(any())) .thenReturn(false); @@ -215,7 +216,7 @@ private FirebaseInstallations getFirebaseInstallations() { @Test public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. @@ -239,7 +240,7 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); @@ -264,7 +265,7 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); @@ -295,7 +296,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. @@ -400,7 +401,7 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E invocation -> { throw new InterruptedException(); }); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -432,7 +433,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws invocation -> { throw new InterruptedException(); }); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -459,7 +460,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { // Update local storage with installation entry that has auth token expired. persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -485,7 +486,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -497,7 +498,7 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { @Test public void testGetAuthToken_PersistedInstallationError_failure() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( executor, @@ -527,7 +528,7 @@ public void testGetAuthToken_PersistedInstallationError_failure() throws Excepti public void testGetAuthToken_fidExists_successful() throws Exception { when(mockPersistedInstallation.readPersistedInstallationEntryValue()) .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -552,8 +553,8 @@ public void testGetAuthToken_fidExists_successful() throws Exception { @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -573,7 +574,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc // Update local storage with a unregistered installation entry to validate that getAuthToken // calls getId to ensure FID registration and returns a valid auth token. persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -588,6 +589,42 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); } + @Test + public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exception { + // Update local storage with an expired installation entry to ensure that generate auth token + // is called. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Mocks error during auth token generation + when(backendClientReturnsOk.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build()); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)) + .thenReturn(/* isExpired*/ true, /*isValid*/ false); + + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + + // Expect exception + try { + Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); + fail("getAuthToken() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.AUTHENTICATION_ERROR); + } + + verify(backendClientReturnsOk, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + } + @Test public void testGetAuthToken_serverError_failure() throws Exception { when(mockPersistedInstallation.readPersistedInstallationEntryValue()) @@ -595,7 +632,7 @@ public void testGetAuthToken_serverError_failure() throws Exception { when(backendClientReturnsError.generateAuthToken( anyString(), anyString(), anyString(), anyString())) .thenThrow(new FirebaseException("Server Error")); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -628,8 +665,8 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -662,23 +699,23 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th AdditionalAnswers.answersWithDelay( 500, (unused) -> - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_3) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build())) .doAnswer( AdditionalAnswers.answersWithDelay( 500, (unused) -> - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_4) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build())) .when(backendClientReturnsOk) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - when(mockUtils.isAuthTokenExpired(any())).thenReturn(false); + when(mockUtils.isAuthTokenExpired(any())).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index f0cefaa133e..e02f2751075 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -17,6 +17,7 @@ import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; @@ -53,10 +54,9 @@ public final class FisAndroidTestConstants { .setFid(TEST_FID_1) .setRefreshToken(TEST_REFRESH_TOKEN) .setAuthToken( - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build()) .setResponseCode(ResponseCode.OK) .build(); @@ -67,19 +67,18 @@ public final class FisAndroidTestConstants { .setFid(TEST_INSTANCE_ID_1) .setRefreshToken(TEST_REFRESH_TOKEN) .setAuthToken( - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build()) .setResponseCode(ResponseCode.OK) .build(); - public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = - InstallationTokenResult.builder() + public static final TokenResult TEST_TOKEN_RESULT = + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_2) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build(); public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE = diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 8a9574d09ee..b0aff260fd1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -31,6 +31,7 @@ import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -261,15 +262,28 @@ private final void doRegistration() { } } + TokenResult tokenResult = null; // Refresh Auth token if needed if (needRefresh) { - fetchAuthTokenFromServer(persistedInstallationEntry); + tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); synchronized (lock) { shouldRefreshAuthToken = false; } } + // If tokenResult is not null and is not successful, it was cleared due to authentication + // error during auth token generation. + if (tokenResult != null && !tokenResult.isSuccessful()) { + triggerOnException( + persistedInstallationEntry, + new FirebaseInstallationsException( + "Failed to generate auth token for this Firebase Installation. Call getId() " + + "to recreate a new Fid and a valid auth token.", + FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); + return; + } + triggerOnStateReached(persistedInstallationEntry); } catch (Exception e) { PersistedInstallationEntry persistedInstallationEntry = @@ -345,28 +359,31 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation } /** Calls the FIS servers to generate an auth token for this Firebase installation. */ - private InstallationTokenResult fetchAuthTokenFromServer( + private TokenResult fetchAuthTokenFromServer( PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); - InstallationTokenResult tokenResult = + TokenResult tokenResult = serviceClient.generateAuthToken( /*apiKey= */ firebaseApp.getOptions().getApiKey(), /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), /*refreshToken= */ persistedInstallationEntry.getRefreshToken()); - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(persistedInstallationEntry.getFirebaseInstallationId()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(tokenResult.getToken()) - .setRefreshToken(persistedInstallationEntry.getRefreshToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); - + if (tokenResult.isSuccessful()) { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + persistedInstallationEntry + .toBuilder() + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(tokenResult.getToken()) + .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + } else { + persistedInstallation.clear(); + } return tokenResult; + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to generate auth token for a Firebase Installation.", diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index fd627a27911..4309a782ffd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -19,12 +19,13 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ public class FirebaseInstallationsException extends FirebaseException { - - // TODO(ankitagj): Improve clear exception handling. + // TODO(ankitagj): Improve exception handling and java doc public enum Status { SDK_INTERNAL_ERROR, - CLIENT_ERROR + CLIENT_ERROR, + + AUTHENTICATION_ERROR } @NonNull private final Status status; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index d0898aabd5a..bc36a20bac6 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -48,7 +48,7 @@ public boolean onStateReached( @Override public boolean onException( PersistedInstallationEntry persistedInstallationEntry, Exception exception) { - if (persistedInstallationEntry.isErrored()) { + if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { resultTaskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index d367ed6e230..22bf2576ea5 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -56,7 +56,7 @@ public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallati return persistedInstallationEntry.isRegistered() && persistedInstallationEntry.getTokenCreationEpochInSecs() + persistedInstallationEntry.getExpiresInSecs() - > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + < currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; } /** Returns current time in seconds. */ diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 65a26534623..bc5947bbe7c 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -26,7 +26,6 @@ import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; -import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import java.io.BufferedReader; import java.io.IOException; @@ -200,7 +199,7 @@ public void deleteFirebaseInstallation( * @param refreshToken a token used to authenticate FIS requests */ @NonNull - public InstallationTokenResult generateAuthToken( + public TokenResult generateAuthToken( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @@ -228,6 +227,17 @@ public InstallationTokenResult generateAuthToken( if (httpResponseCode == 200) { return readGenerateAuthTokenResponse(httpsURLConnection); } + + if (httpResponseCode == 401) { + return TokenResult.builder() + .setResponseCode(TokenResult.ResponseCode.REFRESH_TOKEN_ERROR) + .build(); + } + + if (httpResponseCode == 404) { + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build(); + } + // Usually the FIS server recovers from errors: retry one time before giving up. if (httpResponseCode >= 500 && httpResponseCode < 600) { retryCount++; @@ -259,7 +269,7 @@ private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { // Read the response from the createFirebaseInstallation API. private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); - InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); + TokenResult.Builder tokenResult = TokenResult.builder(); InstallationResponse.Builder builder = InstallationResponse.builder(); reader.beginObject(); while (reader.hasNext()) { @@ -275,15 +285,15 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws while (reader.hasNext()) { String key = reader.nextName(); if (key.equals("token")) { - installationTokenResult.setToken(reader.nextString()); + tokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { - installationTokenResult.setTokenExpirationTimestamp( + tokenResult.setTokenExpirationTimestamp( parseTokenExpirationTimestamp(reader.nextString())); } else { reader.skipValue(); } } - builder.setAuthToken(installationTokenResult.build()); + builder.setAuthToken(tokenResult.build()); reader.endObject(); } else { reader.skipValue(); @@ -295,10 +305,9 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws } // Read the response from the generateAuthToken FirebaseInstallation API. - private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) - throws IOException { + private TokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) throws IOException { JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); - InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); + TokenResult.Builder builder = TokenResult.builder(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); @@ -312,7 +321,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection } reader.endObject(); - return builder.build(); + return builder.setResponseCode(TokenResult.ResponseCode.OK).build(); } // Read the error message from the response. diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index 8bea66ff2e4..0c0463b6f49 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -17,7 +17,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; -import com.google.firebase.installations.InstallationTokenResult; @AutoValue public abstract class InstallationResponse { @@ -39,7 +38,7 @@ public enum ResponseCode { public abstract String getRefreshToken(); @Nullable - public abstract InstallationTokenResult getAuthToken(); + public abstract TokenResult getAuthToken(); @Nullable public abstract ResponseCode getResponseCode(); @@ -65,7 +64,7 @@ public abstract static class Builder { public abstract Builder setRefreshToken(@NonNull String value); @NonNull - public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + public abstract Builder setAuthToken(@NonNull TokenResult value); @NonNull public abstract Builder setResponseCode(@NonNull ResponseCode value); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java new file mode 100644 index 00000000000..a120e418ff0 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java @@ -0,0 +1,73 @@ +// Copyright 2019 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.installations.remote; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class TokenResult { + + public enum ResponseCode { + // Returned on success + OK, + // Auth token cannot be generated for this FID in the request. Because it is not + // registered/found on the FIS server. Recreate a new fid to fetch a valid auth token. + FID_ERROR, + // Refresh token in this request in not accepted by the FIS server. Either it has been blocked + // or changed. Recreate a new fid to fetch a valid auth token. + REFRESH_TOKEN_ERROR, + } + + public boolean isSuccessful() { + return getResponseCode() == ResponseCode.OK; + } + + /** A new FIS Auth-Token, created for this Firebase Installation. */ + @Nullable + public abstract String getToken(); + /** The timestamp, before the auth-token expires for this Firebase Installation. */ + @NonNull + public abstract long getTokenExpirationTimestamp(); + + @Nullable + public abstract ResponseCode getResponseCode(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static TokenResult.Builder builder() { + return new AutoValue_TokenResult.Builder().setTokenExpirationTimestamp(0); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setToken(@NonNull String value); + + @NonNull + public abstract Builder setTokenExpirationTimestamp(long value); + + @NonNull + public abstract Builder setResponseCode(@NonNull ResponseCode value); + + @NonNull + public abstract TokenResult build(); + } +}