-
Notifications
You must be signed in to change notification settings - Fork 617
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
Changes from 4 commits
fc000d2
ce83622
ca75ec8
c1ee9b6
d8492fe
44a28bf
bcf34d3
dcd6c28
f7d9ebf
d72bd7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,7 +82,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 { | ||
|
@@ -110,5 +111,30 @@ 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 isErrored(); | ||
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; | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we outsource this to a dedicated model class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is auto-generated api.txt file. |
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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.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()) | ||
|
@@ -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()); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import java.util.ArrayList; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
|
@@ -270,6 +271,18 @@ private final void doRegistration() { | |
} | ||
} | ||
|
||
// If persisted installation entry is in NOT_GENERATED state, it was cleared due to | ||
// authentication error during auth token generation. | ||
if (persistedInstallationEntry.isNotGenerated()) { | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.", | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); | ||
return; | ||
} | ||
|
||
triggerOnStateReached(persistedInstallationEntry); | ||
} catch (Exception e) { | ||
PersistedInstallationEntry persistedInstallationEntry = | ||
|
@@ -345,28 +358,30 @@ 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 = | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.isSuccessful()) { | ||
persistedInstallation.insertOrUpdatePersistedInstallationEntry( | ||
persistedInstallationEntry | ||
.toBuilder() | ||
.setRegistrationStatus(RegistrationStatus.REGISTERED) | ||
.setAuthToken(tokenResult.getToken()) | ||
.setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) | ||
.setTokenCreationEpochInSecs(creationTime) | ||
.build()); | ||
} else if (tokenResult.isErrored()) { | ||
persistedInstallation.clear(); | ||
} | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about this: if (tokenResult.isSuccessful()) { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
} catch (FirebaseException exception) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which line does throw the FirebaseException? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -228,6 +228,17 @@ public InstallationTokenResult generateAuthToken( | |
if (httpResponseCode == 200) { | ||
return readGenerateAuthTokenResponse(httpsURLConnection); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, the request was not authenticated, what is happening next? |
||
|
||
if (httpResponseCode == 404) { | ||
return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build(); | ||
} | ||
Comment on lines
+237
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: why does this not trigger a deletion of the PersistedInstallationEntry? |
||
|
||
// Usually the FIS server recovers from errors: retry one time before giving up. | ||
if (httpResponseCode >= 500 && httpResponseCode < 600) { | ||
retryCount++; | ||
|
@@ -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(); | ||
|
@@ -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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you added this TokenResult, should we hide the proto class InstallationTokenResult only in the remote package? Because I noticed that FirebaseInstallations class uses both. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both classes are needed in FirebaseInstallations. InstallationTokenResult as the response of the FIS SDK generateAUthToken API. |
||
|
||
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; | ||
} | ||
|
||
public boolean isErrored() { | ||
return getResponseCode() == ResponseCode.FID_ERROR | ||
|| getResponseCode() == ResponseCode.REFRESH_TOKEN_ERROR; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this not the following? |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this always be a bi-state? If yes, then I prefer only having isSuccessful() otherwise when people read the code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed offline, not exactly a bi-state. Updated the method name. |
||
/** A new FIS Auth-Token, created for this Firebase Installation. */ | ||
@Nullable | ||
public abstract String getToken(); | ||
/** | ||
* The amount of time, in seconds, before the auth-token expires for this Firebase Installation. | ||
*/ | ||
@NonNull | ||
public abstract long getTokenExpirationTimestamp(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is a contradiction: |
||
|
||
@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(); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.