Skip to content

Refresh Auth token if expired during getId call. #809

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

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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 @@ -23,6 +23,7 @@
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_3;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID;
import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN;
Expand All @@ -32,6 +33,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -116,7 +118,7 @@ public class FirebaseInstallationsInstrumentedTest {
.setFirebaseInstallationId(TEST_FID_1)
.setAuthToken(TEST_AUTH_TOKEN_2)
.setRefreshToken(TEST_REFRESH_TOKEN)
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2)
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_3)
.setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP)
.setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED)
.build();
Expand Down Expand Up @@ -277,6 +279,36 @@ public void testGetId_PersistedFidError_BackendOk() throws InterruptedException
}
}

@Test
public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
// Update local storage with fid entry that has auth token expired.
persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY);
FirebaseInstallations firebaseInstallations =
new FirebaseInstallations(
mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils);

assertWithMessage("getId Task fails.")
.that(Tasks.await(firebaseInstallations.getId()))
.isNotEmpty();
PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue();
assertWithMessage("Persisted Fid doesn't match")
.that(entryValue.getFirebaseInstallationId())
.isEqualTo(TEST_FID_1);

// Waiting for Task that generates auth token with the FIS Servers
executor.awaitTermination(500, TimeUnit.MILLISECONDS);

// Validate that registration is complete with updated auth token
PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue();
assertWithMessage("Persisted Fid doesn't match")
.that(updatedFidEntry)
.isEqualTo(UPDATED_AUTH_TOKEN_FID_ENTRY);
verify(backendClientReturnsOk, never())
.createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1);
verify(backendClientReturnsOk, times(1))
.generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN);
}

@Test
public void testGetAuthToken_fidDoesNotExist_successful() throws Exception {
FirebaseInstallations firebaseInstallations =
Expand Down Expand Up @@ -409,11 +441,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce(
// triggered simultaneously. Task2 waits for Task1 to complete. On Task1 completion, task2 reads
// the UPDATED_AUTH_TOKEN_FID_ENTRY by Task1 on execution.
when(mockPersistedFid.readPersistedFidEntryValue())
.thenReturn(
EXPIRED_AUTH_TOKEN_ENTRY,
EXPIRED_AUTH_TOKEN_ENTRY,
EXPIRED_AUTH_TOKEN_ENTRY,
UPDATED_AUTH_TOKEN_FID_ENTRY);
.thenReturn(EXPIRED_AUTH_TOKEN_ENTRY, UPDATED_AUTH_TOKEN_FID_ENTRY);
FirebaseInstallations firebaseInstallations =
new FirebaseInstallations(
mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ public final class FisAndroidTestConstants {

public static final long TEST_CREATION_TIMESTAMP_1 = 2000L;
public static final long TEST_CREATION_TIMESTAMP_2 = 2000L;
public static final long TEST_CREATION_TIMESTAMP_3 = 2L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,14 @@ String getName() {
*/
private PersistedFidEntry getPersistedFid() throws FirebaseInstallationsException {
PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue();
if (persistedFidMissingOrInErrorState(persistedFidEntry)) {
if (persistedFidEntry == null
|| persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTER_ERROR) {
throw new FirebaseInstallationsException(
"Failed to get existing fid.", FirebaseInstallationsException.Status.CLIENT_ERROR);
}
return persistedFidEntry;
}

private static boolean persistedFidMissingOrInErrorState(PersistedFidEntry persistedFidEntry) {
return persistedFidEntry == null
|| persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTER_ERROR;
}

@NonNull
private static <F, T> Continuation<F, T> orElse(@NonNull Supplier<T> supplier) {
return t -> {
Expand Down Expand Up @@ -235,31 +231,50 @@ private void persistFid(String fid) throws FirebaseInstallationsException {
}

/**
* Registers the FID with FIS servers if FID is in UNREGISTERED state.
* Registers the FID with FIS servers if FID is in UNREGISTERED state. Also, refreshes auth token
* if expired.
*
* <p>Updates FID registration status to PENDING to avoid multiple network calls to FIS Servers.
*/
private Task<String> registerFidIfNecessary(
PersistedFidEntry persistedFidEntry, AwaitListener listener) {
String fid = persistedFidEntry.getFirebaseInstallationId();

// Check if the fid is unregistered
if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.UNREGISTERED) {
updatePersistedFidWithPendingStatus(fid);
// If FID registration is in complete/pending state or FID auth token is valid, update the
// listener if awaiting and return the persisted fid.
if (persistedFidEntry.getRegistrationStatus() != RegistrationStatus.UNREGISTERED
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 add a helper for this? PersistedFidEntry#isRegistered

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, as this method could be reused in getId and delete APIs.

&& !isAuthTokenExpired(persistedFidEntry)) {
return updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
}

// Update FID registration status to PENDING to avoid multiple network calls to FIS Servers.
persistedFid.insertOrUpdatePersistedFidEntry(
persistedFidEntry.toBuilder().setRegistrationStatus(RegistrationStatus.PENDING).build());
Copy link
Contributor

Choose a reason for hiding this comment

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

you need to keep "reading cache status" and "updating cache status" in one atomic section, to avoid duplicated network calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. PTAL.


// If fid registration for this firebase installation is incomplete without an expired auth
// token, execute Fid registration.
if (!isAuthTokenExpired(persistedFidEntry)) {
executeFidRegistration(persistedFidEntry, listener);
} else {
updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
return Tasks.forResult(fid);
}

// If auth token is expired, refresh FID auth token with FIS servers in a background thread and
// updates the listener on
Copy link
Contributor

Choose a reason for hiding this comment

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

nit : in one line.

// completion
Task<InstallationTokenResult> task = Tasks.call(executor, () -> refreshAuthTokenIfNecessary(1));
if (listener != null) {
task.addOnCompleteListener((unused) -> listener.onSuccess());
}
return Tasks.forResult(fid);
}

private void updateAwaitListenerIfRegisteredFid(
private Task<String> updateAwaitListenerIfRegisteredFid(
PersistedFidEntry persistedFidEntry, AwaitListener listener) {
if (listener != null
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED) {
listener.onSuccess();
}
return Tasks.forResult(persistedFidEntry.getFirebaseInstallationId());
}

/**
Expand All @@ -273,14 +288,6 @@ private void executeFidRegistration(PersistedFidEntry persistedFidEntry, AwaitLi
}
}

private void updatePersistedFidWithPendingStatus(String fid) {
persistedFid.insertOrUpdatePersistedFidEntry(
PersistedFidEntry.builder()
.setFirebaseInstallationId(fid)
.setRegistrationStatus(RegistrationStatus.PENDING)
.build());
}

/** Registers the created Fid with FIS servers and update the shared prefs. */
private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry)
throws FirebaseInstallationsException {
Expand Down Expand Up @@ -355,7 +362,8 @@ private InstallationTokenResult getValidAuthToken(PersistedFidEntry persistedFid

private boolean isPersistedFidRegistered(PersistedFidEntry persistedFidEntry) {
return persistedFidEntry != null
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED;
&& (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED
|| persistedFidEntry.getRegistrationStatus() == RegistrationStatus.PENDING);
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be removed now, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. We need this check. With this new flow - both getId and getAuthToken need this.

getAuthToken uses it with persistedFid in REGISTERED state.
getId uses it with persistedFid in PENDING state.

}

/** Calls the FIS servers to generate an auth token for this Firebase installation. */
Expand All @@ -382,6 +390,12 @@ private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persi

return tokenResult;
} catch (FirebaseInstallationServiceException exception) {
persistedFid.insertOrUpdatePersistedFidEntry(
persistedFidEntry
.toBuilder()
.setRegistrationStatus(RegistrationStatus.REGISTER_ERROR)
.build());

throw new FirebaseInstallationsException(
"Failed to generate auth token for a Firebase Installation.",
FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR);
Expand Down