-
Notifications
You must be signed in to change notification settings - Fork 616
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
Changes from 7 commits
18cc029
c0580b1
8558f73
0fb2666
b727b35
60f4023
63da567
c26adc7
4afc7e5
86e3728
4f64c09
62098a7
f7457d3
fe2577c
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 |
---|---|---|
|
@@ -89,7 +89,7 @@ public class FirebaseInstallationsInstrumentedTest { | |
.setFirebaseInstallationId(TEST_FID_1) | ||
.setAuthToken(TEST_AUTH_TOKEN) | ||
.setRefreshToken(TEST_REFRESH_TOKEN) | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) | ||
.setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) | ||
.setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) | ||
.build(); | ||
|
@@ -99,7 +99,7 @@ public class FirebaseInstallationsInstrumentedTest { | |
.setFirebaseInstallationId(TEST_FID_1) | ||
.setAuthToken(TEST_AUTH_TOKEN) | ||
.setRefreshToken(TEST_REFRESH_TOKEN) | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) | ||
.setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) | ||
.setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) | ||
.build(); | ||
|
@@ -109,7 +109,7 @@ public class FirebaseInstallationsInstrumentedTest { | |
.setFirebaseInstallationId(TEST_FID_1) | ||
.setAuthToken("") | ||
.setRefreshToken("") | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) | ||
.setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) | ||
.setExpiresInSecs(0) | ||
.setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) | ||
.build(); | ||
|
@@ -190,7 +190,7 @@ public void testGetId_PersistedFidOk_BackendOk() throws Exception { | |
mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); | ||
|
||
// No exception, means success. | ||
assertWithMessage("getId Task fails.") | ||
assertWithMessage("getId Task failed") | ||
.that(Tasks.await(firebaseInstallations.getId())) | ||
.isNotEmpty(); | ||
PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); | ||
|
@@ -217,7 +217,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { | |
mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); | ||
|
||
// No exception, means success. | ||
assertWithMessage("getId Task fails.") | ||
assertWithMessage("getId Task failed") | ||
.that(Tasks.await(firebaseInstallations.getId())) | ||
.isNotEmpty(); | ||
PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); | ||
|
@@ -290,6 +290,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 failed") | ||
.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 = | ||
|
@@ -418,18 +448,13 @@ public void testGetAuthToken_serverError_failure() throws Exception { | |
@Test | ||
public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() | ||
throws Exception { | ||
// Using mockPersistedFid to ensure the order of returning persistedFidEntry to 2 tasks | ||
// 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); | ||
// Update local storage with fid entry that has auth token expired. When 2 tasks are triggered | ||
// simultaneously, Task2 waits for Task1 to complete. On Task1 completion, task2 reads the | ||
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. nit: "reads the" has too many (two) spaces |
||
// UPDATED_AUTH_TOKEN_FID_ENTRY by Task1 on execution. | ||
persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); | ||
FirebaseInstallations firebaseInstallations = | ||
new FirebaseInstallations( | ||
mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); | ||
mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); | ||
|
||
// Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option | ||
Task<InstallationTokenResult> task1 = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,8 +123,7 @@ public Task<String> getId() { | |
private Task<String> getId(AwaitListener awaitListener) { | ||
return Tasks.call(executor, this::getPersistedFid) | ||
.continueWith(orElse(this::createAndPersistNewFid)) | ||
.onSuccessTask( | ||
persistedFidEntry -> registerFidIfNecessary(persistedFidEntry, awaitListener)); | ||
.onSuccessTask(unused -> registerFidIfNecessary(awaitListener)); | ||
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 this and the continueWith need an executor or they'll run on the main thread |
||
} | ||
|
||
/** | ||
|
@@ -179,18 +178,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 -> { | ||
|
@@ -235,50 +230,62 @@ 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); | ||
executeFidRegistration(persistedFidEntry, listener); | ||
} else { | ||
updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener); | ||
} | ||
private Task<String> registerFidIfNecessary(AwaitListener listener) { | ||
synchronized (persistedFid) { | ||
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. if you're going to start synchronizing these instead of piping the values through a Task chain then you should add @GuardedBy annotations http://errorprone.info/bugpattern/GuardedBy 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, I am locking on persistedFid and not persistedFidEntry( was piping it earlier on this value through task). Also, I cannot synchronize on persistedFid throughout this 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. Yes, my point was that synchronization isn't necessarily required if you're just piping them through a Task call stack. Why are we synchronizing on it here if we can't do it consistently? What benefit does it provide here that it doesn't provide elsewhere? Can we document that more explicitly? 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. PENDING state was introduced to avoid multiple network calls for getId(). Registration status changes from UNREGISTERED to PENDING before triggering the network call. Need for synchronization: Reading the existing registration status and updating it to PENDING should be atomic. That's the block i tried to synchronize. I think piping wont solve the atomicity issue. 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. ugh, this is too complicated I see two similar potential issues here:
(also sidenote: given that getAuthToken is jumping straight to a worker thread it seems weird to have it synchronized, esp. since it's the only block synchronizing on |
||
PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); | ||
ciarand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
String fid = persistedFidEntry.getFirebaseInstallationId(); | ||
|
||
// If FID registration status is PENDING, return the persisted fid. This avoids multiple | ||
// network calls to FIS Servers. | ||
if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.PENDING) { | ||
ankitaj224 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return Tasks.forResult(fid); | ||
} | ||
|
||
return Tasks.forResult(fid); | ||
} | ||
// If FID registration status is REGISTERED and auth token is valid, return the | ||
// persisted fid. | ||
if (persistedFid.isFidRegistered(persistedFidEntry) | ||
&& !isAuthTokenExpired(persistedFidEntry)) { | ||
|
||
private void updateAwaitListenerIfRegisteredFid( | ||
PersistedFidEntry persistedFidEntry, AwaitListener listener) { | ||
if (listener != null | ||
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED) { | ||
listener.onSuccess(); | ||
} | ||
} | ||
// Update the listener if awaiting | ||
if (listener != null) { | ||
listener.onSuccess(); | ||
} | ||
return Tasks.forResult(fid); | ||
} | ||
|
||
/** | ||
* Registers the FID with FIS servers in a background thread and updates the listener on | ||
* completion. | ||
*/ | ||
private void executeFidRegistration(PersistedFidEntry persistedFidEntry, AwaitListener listener) { | ||
Task<Void> task = Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); | ||
if (listener != null) { | ||
task.addOnCompleteListener(listener); | ||
} | ||
} | ||
// Update FID registration status to PENDING to avoid multiple network calls to FIS Servers. | ||
persistedFid.insertOrUpdatePersistedFidEntry( | ||
persistedFidEntry.toBuilder().setRegistrationStatus(RegistrationStatus.PENDING).build()); | ||
|
||
// If fid registration for this firebase installation is incomplete without an expired auth | ||
// token, execute Fid registration. | ||
if (!isAuthTokenExpired(persistedFidEntry)) { | ||
Task<Void> fidRegistrationTask = | ||
Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); | ||
|
||
// Update the listener if awaiting | ||
if (listener != null) { | ||
fidRegistrationTask.addOnCompleteListener(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 completion | ||
Task<InstallationTokenResult> refreshAuthTokenTask = | ||
Tasks.call(executor, () -> refreshAuthTokenIfNecessary(FORCE_REFRESH)); | ||
|
||
private void updatePersistedFidWithPendingStatus(String fid) { | ||
persistedFid.insertOrUpdatePersistedFidEntry( | ||
PersistedFidEntry.builder() | ||
.setFirebaseInstallationId(fid) | ||
.setRegistrationStatus(RegistrationStatus.PENDING) | ||
.build()); | ||
// Update the listener if awaiting | ||
if (listener != null) { | ||
refreshAuthTokenTask.addOnCompleteListener((unused) -> listener.onSuccess()); | ||
} | ||
return Tasks.forResult(fid); | ||
} | ||
} | ||
|
||
/** Registers the created Fid with FIS servers and update the shared prefs. */ | ||
|
@@ -320,7 +327,9 @@ private InstallationTokenResult refreshAuthTokenIfNecessary(int authTokenOption) | |
|
||
PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); | ||
|
||
if (!isPersistedFidRegistered(persistedFidEntry)) { | ||
if (persistedFidEntry == null | ||
|| (persistedFidEntry.getRegistrationStatus() != RegistrationStatus.REGISTERED | ||
&& persistedFidEntry.getRegistrationStatus() != RegistrationStatus.PENDING)) { | ||
throw new FirebaseInstallationsException( | ||
"Firebase Installation is not registered.", | ||
FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); | ||
|
@@ -353,11 +362,6 @@ private InstallationTokenResult getValidAuthToken(PersistedFidEntry persistedFid | |
.build(); | ||
} | ||
|
||
private boolean isPersistedFidRegistered(PersistedFidEntry persistedFidEntry) { | ||
return persistedFidEntry != null | ||
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED; | ||
} | ||
|
||
/** Calls the FIS servers to generate an auth token for this Firebase installation. */ | ||
private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persistedFidEntry) | ||
throws FirebaseInstallationsException { | ||
|
@@ -382,6 +386,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); | ||
|
@@ -409,7 +419,7 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio | |
|
||
PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); | ||
|
||
if (isPersistedFidRegistered(persistedFidEntry)) { | ||
if (persistedFid.isFidRegistered(persistedFidEntry)) { | ||
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 isn't right, is it? if 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. if 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. Changes this. |
||
// Call the FIS servers to delete this firebase installation id. | ||
try { | ||
serviceClient.deleteFirebaseInstallation( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,4 +134,10 @@ public boolean clear() { | |
private String getSharedPreferencesKey(String key) { | ||
return String.format("%s|%s", persistenceKey, key); | ||
} | ||
|
||
@NonNull | ||
public boolean isFidRegistered(@Nullable PersistedFidEntry persistedFidEntry) { | ||
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. imo this should go on PersistedFidEntry or some sort of static util class... Otherwise it feels like we're just putting it here for convenience 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. Updated. PTAL. |
||
return persistedFidEntry != null | ||
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.