Skip to content

getAuthToken error handling for 401 & 404 response code #961

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 10 commits into from
Nov 21, 2019
27 changes: 26 additions & 1 deletion firebase-installations/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ package com.google.firebase.installations.local {
}

public enum PersistedInstallation.RegistrationStatus {
enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus AUTHENTICATION_TOKEN_ERROR;
enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus NOT_GENERATED;
enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTERED;
enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTER_ERROR;
Expand All @@ -54,6 +55,7 @@ package com.google.firebase.installations.local {
method @Nullable public abstract String getRefreshToken();
method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus getRegistrationStatus();
method public abstract long getTokenCreationEpochInSecs();
method public boolean isAuthTokenErrored();
method public boolean isErrored();
method public boolean isNotGenerated();
method public boolean isRegistered();
Expand Down Expand Up @@ -81,7 +83,7 @@ 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 {
Expand Down Expand Up @@ -110,5 +112,28 @@ 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 @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;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -588,6 +589,41 @@ 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(true, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please add comments here to make it easier to understand what these values are:
e.g.

then Return( true /* isExpired /, false / isValid */)


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.CLIENT_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())
Expand Down Expand Up @@ -662,19 +698,19 @@ 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -82,6 +83,13 @@ public final class FisAndroidTestConstants {
.setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1)
.build();

public static final TokenResult TEST_TOKEN_RESULT =
TokenResult.builder()
.setToken(TEST_AUTH_TOKEN_2)
.setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP)
.setResponseCode(TokenResult.ResponseCode.OK)
.build();

public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE =
InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -270,6 +271,19 @@ private final void doRegistration() {
}
}

// If persisted fid is in AUTH_TOKEN_ERROR state, clear the stored fid and update the
// listener with an exception.
if (persistedInstallationEntry.isAuthTokenErrored()) {
persistedInstallation.clear();
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.CLIENT_ERROR));
return;
}

triggerOnStateReached(persistedInstallationEntry);
} catch (Exception e) {
PersistedInstallationEntry persistedInstallationEntry =
Expand Down Expand Up @@ -345,28 +359,37 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation
}

/** Calls the FIS servers to generate an auth token for this Firebase installation. */
private InstallationTokenResult fetchAuthTokenFromServer(
PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException {
private void 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());

return tokenResult;
if (tokenResult.getResponseCode() == TokenResult.ResponseCode.FID_ERROR
|| tokenResult.getResponseCode() == TokenResult.ResponseCode.REFRESH_TOKEN_ERROR) {
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
persistedInstallationEntry
.toBuilder()
.setRegistrationStatus(RegistrationStatus.AUTHENTICATION_TOKEN_ERROR)
.build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the auth error case, if anyways you will clear the cache, why do you update it to AUTHENTICATION_TOKEN_ERROR and then clear it in doRegistration()?

An alternative is to clear it here, and like your old code, still return TokenResult from this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Changed it. However, I retained the return type as void because it is unused by the caller of the method.

}

if (tokenResult.getResponseCode() == TokenResult.ResponseCode.OK) {
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
persistedInstallationEntry
.toBuilder()
.setRegistrationStatus(RegistrationStatus.REGISTERED)
.setAuthToken(tokenResult.getToken())
.setExpiresInSecs(tokenResult.getTokenExpirationTimestamp())
.setTokenCreationEpochInSecs(creationTime)
.build());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls use
if ()
else if ()
else

for completeness


} catch (FirebaseException exception) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which line does throw the FirebaseException?
It looks like currently the entire method is inside a try-catch block...
If that is the case, can we throw the exception and catch it outside?
or maybe (preferably) can we catch only the lines that can cause the exception to be thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TODO to improve Exception handling in general.

throw new FirebaseInstallationsException(
"Failed to generate auth token for a Firebase Installation.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public boolean onStateReached(
@Override
public boolean onException(
PersistedInstallationEntry persistedInstallationEntry, Exception exception) {
if (persistedInstallationEntry.isErrored()) {
if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isAuthTokenErrored()) {
resultTaskCompletionSource.trySetException(exception);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public enum RegistrationStatus {
* locally before registering with FIS servers.
*/
REGISTER_ERROR,
AUTHENTICATION_TOKEN_ERROR,
}

private static final String SHARED_PREFS_NAME = "PersistedInstallation";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public boolean isNotGenerated() {
return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED;
}

public boolean isAuthTokenErrored() {
return getRegistrationStatus()
== PersistedInstallation.RegistrationStatus.AUTHENTICATION_TOKEN_ERROR;
}

@NonNull
public abstract Builder toBuilder();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,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,
Expand Down Expand Up @@ -228,6 +228,17 @@ public InstallationTokenResult generateAuthToken(
if (httpResponseCode == 200) {
return readGenerateAuthTokenResponse(httpsURLConnection);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to not just return the response?

}

if (httpResponseCode == 401) {
return TokenResult.builder()
.setResponseCode(TokenResult.ResponseCode.REFRESH_TOKEN_ERROR)
.build();
}
Comment on lines +231 to +235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the request was not authenticated, what is happening next?
Are we deleting the PersistedInstallationEntry?
If yes, where is this done?
Can you please add a comment what REFRES_TOKEN_ERROR will trigger elsewhere?


if (httpResponseCode == 404) {
return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build();
}
Comment on lines +237 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why does this not trigger a deletion of the PersistedInstallationEntry?
and if that is done somewhere else, could you please add a comment here, that the FID_ERROR will trigger a reset of the FIS SDK elsewhere?


// Usually the FIS server recovers from errors: retry one time before giving up.
if (httpResponseCode >= 500 && httpResponseCode < 600) {
retryCount++;
Expand Down Expand Up @@ -295,10 +306,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();
Expand All @@ -312,7 +322,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection
}
reader.endObject();

return builder.build();
return builder.setResponseCode(TokenResult.ResponseCode.OK).build();
}

// Read the error message from the response.
Expand Down
Loading