mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-06 20:54:26 +08:00
[feat] Refresh Gmail access tokens when needed
Summary: This is a small patch but it's pretty complex, because of the numbers of moving parts. Gmail has two types of tokens, access and refresh tokens. Access tokens have a limited shelf life of one hour. After that they expire and you need to use your refresh token to get a new one. We've decided to do the access token generation on the server, because we don't feel comfortable giving our users both our Google client id and secret. To do that, I've added an endpoint, `/gmail/auth/refresh` which returns a valid access token as well as an expiration date for the token. The only place where we handle token expiration is in the sync workers. Before trying opening a new connection we check if our access token is expired. If yes, we get a new one from the API. If there's an issue doing this, we notify N1 using `NylasAPIHelpers.handleAuthenticationFailure`. There's a second patch for N1 with tiny related fixes. Test Plan: Tested manually. Will need to test more in the real world. Reviewers: evan, jackie, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D3522
This commit is contained in:
parent
86a2b13730
commit
101b99f4a7
3 changed files with 78 additions and 34 deletions
|
@ -17,6 +17,7 @@ const imapSmtpSettings = Joi.object().keys({
|
|||
|
||||
const resolvedGmailSettings = Joi.object().keys({
|
||||
xoauth2: Joi.string().required(),
|
||||
expiry_date: Joi.number().integer().required(),
|
||||
}).required();
|
||||
|
||||
const exchangeSettings = Joi.object().keys({
|
||||
|
@ -26,7 +27,7 @@ const exchangeSettings = Joi.object().keys({
|
|||
}).required();
|
||||
|
||||
const USER_ERRORS = {
|
||||
AUTH_500: "Please contact support@nylas.com. An unforseen error has occurred.",
|
||||
AUTH_500: "Please contact support@nylas.com. An unforeseen error has occurred.",
|
||||
IMAP_AUTH: "Incorrect username or password",
|
||||
IMAP_RETRY: "We were unable to reach your mail provider. Please try again.",
|
||||
}
|
||||
|
@ -81,6 +82,7 @@ module.exports = {
|
|||
}
|
||||
connectionCredentials = {
|
||||
xoauth2: settings.xoauth2,
|
||||
expiry_date: settings.expiry_date,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ const Capabilities = {
|
|||
Sort: 'SORT',
|
||||
}
|
||||
|
||||
const ONE_HOUR_SECS = 60 * 60;
|
||||
|
||||
class IMAPConnection extends EventEmitter {
|
||||
|
||||
static connect(...args) {
|
||||
|
@ -46,6 +48,13 @@ class IMAPConnection extends EventEmitter {
|
|||
this._connectPromise = null;
|
||||
}
|
||||
|
||||
static generateXOAuth2Token(username, accessToken) {
|
||||
// See https://developers.google.com/gmail/xoauth2_protocol
|
||||
// for more details.
|
||||
const s = `user=${username}\x01auth=Bearer ${accessToken}\x01\x01`
|
||||
return new Buffer(s).toString('base64');
|
||||
}
|
||||
|
||||
get account() {
|
||||
return this._account
|
||||
}
|
||||
|
@ -89,6 +98,7 @@ class IMAPConnection extends EventEmitter {
|
|||
if (err) { return reject(err) }
|
||||
delete result.password;
|
||||
result.xoauth2 = token;
|
||||
result.expiry_date = Math.floor(Date.now() / 1000) + ONE_HOUR_SECS;
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
|
@ -99,6 +109,7 @@ class IMAPConnection extends EventEmitter {
|
|||
if (this._settings.xoauth2) {
|
||||
delete result.password;
|
||||
result.xoauth2 = this._settings.xoauth2;
|
||||
result.expiry_date = this._settings.expiry_date;
|
||||
}
|
||||
|
||||
return Promise.resolve(result);
|
||||
|
|
|
@ -9,6 +9,8 @@ const FetchFolderList = require('./imap/fetch-folder-list')
|
|||
const FetchMessagesInFolder = require('./imap/fetch-messages-in-folder')
|
||||
const SyncbackTaskFactory = require('./syncback-task-factory')
|
||||
const SyncMetricsReporter = require('./sync-metrics-reporter');
|
||||
const {NylasAPI, N1CloudAPI, NylasAPIRequest, NylasAPIHelpers} = require('nylas-exports');
|
||||
|
||||
|
||||
|
||||
const RESTART_THRESHOLD = 10
|
||||
|
@ -76,6 +78,68 @@ class SyncWorker {
|
|||
return this._db.Folder.find({where: {role: ['all', 'inbox']}})
|
||||
}
|
||||
|
||||
async ensureConnection() {
|
||||
if (this._conn) {
|
||||
return await this._conn.connect();
|
||||
}
|
||||
const settings = this._account.connectionSettings;
|
||||
let credentials = this._account.decryptedCredentials();
|
||||
|
||||
if (!settings || !settings.imap_host) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection settings for this account.");
|
||||
}
|
||||
if (!credentials) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection credentials for this account.");
|
||||
}
|
||||
|
||||
if (this._account.provider == 'gmail') {
|
||||
// Before creating a connection, check if the our access token expired.
|
||||
// If yes, we need to get a new one.
|
||||
const currentUnixDate = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (currentUnixDate > credentials.expiry_date) {
|
||||
const req = new NylasAPIRequest({
|
||||
api: N1CloudAPI,
|
||||
options: {
|
||||
path: `/auth/gmail/refresh`,
|
||||
method: 'POST',
|
||||
accountId: this._account.id,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const newCredentials = await req.run()
|
||||
this._account.setCredentials(newCredentials);
|
||||
await this._account.save()
|
||||
|
||||
credentials = newCredentials;
|
||||
this._logger.info("Refreshed and updated access token.");
|
||||
} catch (error) {
|
||||
const accountToken = NylasAPI.accessTokenForAccountId(this._account.id);
|
||||
NylasAPIHelpers.handleAuthenticationFailure('/auth/gmail/refresh', accountToken);
|
||||
throw new Error("Unable to authenticate to the remote server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const conn = new IMAPConnection({
|
||||
db: this._db,
|
||||
settings: Object.assign({}, settings, credentials),
|
||||
logger: this._logger,
|
||||
});
|
||||
|
||||
conn.on('mail', () => {
|
||||
this._onConnectionIdleUpdate();
|
||||
})
|
||||
conn.on('update', () => {
|
||||
this._onConnectionIdleUpdate();
|
||||
})
|
||||
conn.on('queue-empty', () => {});
|
||||
|
||||
this._conn = conn;
|
||||
return await this._conn.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of at most 100 Syncback requests, sorted by creation date
|
||||
* (older first) and by how they affect message IMAP uids.
|
||||
|
@ -158,39 +222,6 @@ class SyncWorker {
|
|||
)
|
||||
}
|
||||
|
||||
async ensureConnection() {
|
||||
if (this._conn) {
|
||||
return await this._conn.connect();
|
||||
}
|
||||
const settings = this._account.connectionSettings;
|
||||
const credentials = this._account.decryptedCredentials();
|
||||
|
||||
if (!settings || !settings.imap_host) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection settings for this account.");
|
||||
}
|
||||
if (!credentials) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection credentials for this account.");
|
||||
}
|
||||
|
||||
const conn = new IMAPConnection({
|
||||
db: this._db,
|
||||
account: this._account,
|
||||
settings: Object.assign({}, settings, credentials),
|
||||
logger: this._logger,
|
||||
});
|
||||
|
||||
conn.on('mail', () => {
|
||||
this._onConnectionIdleUpdate();
|
||||
})
|
||||
conn.on('update', () => {
|
||||
this._onConnectionIdleUpdate();
|
||||
})
|
||||
conn.on('queue-empty', () => {});
|
||||
|
||||
this._conn = conn;
|
||||
return await this._conn.connect();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
clearTimeout(this._syncTimer);
|
||||
this._syncTimer = null;
|
||||
|
|
Loading…
Add table
Reference in a new issue