[local-sync] Refresh Google OAuth2 tokens when Invalid Credentials occurs in sync loop

Summary:
Previously, we would only refresh Google OAuth2 access tokens at the
beginning of the sync loop, and _only_ if the access token had already
expired. This meant that if an access token expired in the middle of a
sync loop iteration, the user would get prompted with the reauth red box
for their account and would have to either go through the oauth flow
again or restart the app for sync to continue.

This diff makes two changes:

1. Adds 5min of padding to the refresh window, so if a token will expire
in <5min, we'll go ahead and refresh the token. This will reduce the
possibility that an access token can expire during a sync loop
iteration.

2. Catches Invalid Credentials IMAPAuthenticationErrors for Gmail
accounts and forces a token refresh on the next sync loop.

These should prevent a user from _ever_ having to reauth their Gmail
account unless the refresh token is revoked, or we encounter some other
permanent error trying to refresh the token.

Fixes T7775 (at least some cases)

Test Plan: manual

Reviewers: khamidou, evan, juan

Reviewed By: juan

Maniphest Tasks: T7775, T7755

Differential Revision: https://phab.nylas.com/D3908
This commit is contained in:
Christine Spang 2017-02-15 08:46:12 -08:00
parent 56422eee10
commit dce9743283

View file

@ -45,6 +45,7 @@ class SyncWorker {
this._numRetries = 0;
this._numTimeoutErrors = 0;
this._socketTimeout = IMAPConnection.DefaultSocketTimeout;
this._requireTokenRefresh = false
this._syncTimer = setTimeout(() => {
// TODO this is currently a hack to keep N1's account in sync and notify of
@ -144,7 +145,12 @@ class SyncWorker {
}
const currentUnixDate = Math.floor(Date.now() / 1000);
if (currentUnixDate > credentials.expiry_date) {
if (this._requireTokenRefresh && (credentials.expiry_date > currentUnixDate)) {
console.warn("ensureAccessToken: got Invalid Credentials from server but token is not expired");
}
// try to avoid tokens expiring during the sync loop
const expiryDatePlusSlack = credentials.expiry_date - (5 * 60);
if (this._requireTokenRefresh || (currentUnixDate > expiryDatePlusSlack)) {
const req = new NylasAPIRequest({
api: N1CloudAPI,
options: {
@ -157,6 +163,7 @@ class SyncWorker {
const newCredentials = await req.run()
this._account.setCredentials(newCredentials);
await this._account.save();
this._requireTokenRefresh = false
return newCredentials;
}
return null
@ -181,6 +188,8 @@ class SyncWorker {
if (isNonPermanentError) {
throw new IMAPErrors.IMAPTransientAuthenticationError(`Server error when trying to refresh token.`);
} else {
// sync worker is persistent across reauths, so need to clear this flag
this._requireTokenRefresh = false
throw new IMAPErrors.IMAPAuthenticationError(`Unable to refresh access token`);
}
}
@ -312,6 +321,19 @@ class SyncWorker {
}
async _onSyncError(error) {
// We try to refresh Google OAuth2 access tokens in advance, but sometimes
// it doesn't work (e.g. the token expires during the sync loop). In this
// case, we need to immediately restart the sync loop & refresh the token.
//
// These error messages look like "Error: Invalid credentials (Failure)"
const isExpiredTokenError = (this._account.provider === "gmail" &&
error instanceof IMAPErrors.IMAPAuthenticationError &&
/invalid credentials/i.test(error.message))
if (isExpiredTokenError) {
this._requireTokenRefresh = true
return
}
this._closeConnections()
const errorJSON = error.toJSON()
@ -398,7 +420,8 @@ class SyncWorker {
// interrupted or a sync was requested
const shouldSyncImmediately = (
moreToSync ||
this._interrupted
this._interrupted ||
this._requireTokenRefresh
)
interval = shouldSyncImmediately ? 1 : SYNC_LOOP_INTERVAL_MS;
}