mail/src/js/service/auth.js

366 lines
12 KiB
JavaScript

'use strict';
var ngModule = angular.module('woServices');
ngModule.service('auth', Auth);
module.exports = Auth;
var axe = require('axe-logger'),
cfg = require('../app-config').config,
str = require('../app-config').string;
var APP_CONFIG_DB_NAME = 'app-config';
var EMAIL_ADDR_DB_KEY = 'emailaddress';
var USERNAME_DB_KEY = 'username';
var REALNAME_DB_KEY = 'realname';
var PASSWD_DB_KEY = 'password';
var IMAP_DB_KEY = 'imap';
var SMTP_DB_KEY = 'smtp';
/**
* The Auth BO handles the rough edges and gaps between user/password authentication
* and OAuth via Chrome Identity API.
* Typical usage:
* var auth = new Auth(...);
* auth.setCredentials(...); // during the account setup
* auth.getEmailAddress(...); // called from the login controller to determine if there is already a user present on the device
* auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP,
* username, password / oauth token, IMAP/SMTP server host names, ...
*/
function Auth(appConfigStore, oauth, pgp) {
this._appConfigStore = appConfigStore;
this._oauth = oauth;
this._pgp = pgp;
this._initialized = false;
}
/**
* Initialize the service
*/
Auth.prototype.init = function() {
var self = this;
return self._appConfigStore.init(APP_CONFIG_DB_NAME).then(function() {
self._initialized = true;
});
};
/**
* Check if the service has been initialized.
*/
Auth.prototype.isInitialized = function() {
return this._initialized;
};
/**
* Retrieves credentials and IMAP/SMTP settings:
* 1) Fetches the credentials from disk, then...
* 2 a) ... in an oauth setting, retrieves a fresh oauth token from the Chrome Identity API.
* 2 b) ... in a user/passwd setting, does not need to do additional work.
* 3) Loads the intermediate certs from the configuration.
*/
Auth.prototype.getCredentials = function() {
var self = this;
if (!self.emailAddress) {
// we're not yet initialized, so let's load our stuff from disk
return self._loadCredentials().then(chooseLogin);
}
return chooseLogin();
function chooseLogin() {
if (self.useOAuth(self.imap.host) && !self.password) {
// oauth login
return self.getOAuthToken().then(done);
}
if (self.passwordNeedsDecryption) {
// decrypt password
return self._pgp.decrypt(self.password, undefined).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature of encrypted password failed!');
}
self.passwordNeedsDecryption = false;
self.password = pt.decrypted;
}).then(done);
}
return done();
}
function done() {
return new Promise(function(resolve) {
var credentials = {
imap: {
secure: self.imap.secure,
port: self.imap.port,
host: self.imap.host,
ca: self.imap.ca,
auth: {
user: self.username,
xoauth2: self.oauthToken, // password or oauthToken is undefined
pass: self.password
}
},
smtp: {
secure: self.smtp.secure,
port: self.smtp.port,
host: self.smtp.host,
ca: self.smtp.ca,
auth: {
user: self.username,
xoauth2: self.oauthToken,
pass: self.password // password or oauthToken is undefined
}
}
};
resolve(credentials);
});
}
};
/**
* Set the credentials
*
* @param {String} options.emailAddress The email address
* @param {String} options.username The user name
* @param {String} options.realname The user's real name
* @param {String} options.password The password, only in user/passwd setting
* @param {String} options.smtp The smtp settings (host, port, secure)
* @param {String} options.imap The imap settings (host, port, secure)
*/
Auth.prototype.setCredentials = function(options) {
this.credentialsDirty = true;
this.emailAddress = options.emailAddress;
this.username = options.username;
this.realname = options.realname ? options.realname : '';
this.password = options.password;
this.smtp = options.smtp; // host, port, secure, ca
this.imap = options.imap; // host, port, secure, ca
};
Auth.prototype.storeCredentials = function() {
var self = this;
if (!self.credentialsDirty) {
// nothing to store if credentials not dirty
return new Promise(function(resolve) {
resolve();
});
}
// persist the config
var storeSmtp = self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY);
var storeImap = self._appConfigStore.storeList([self.imap], IMAP_DB_KEY);
var storeEmailAddress = self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY);
var storeUsername = self._appConfigStore.storeList([self.username], USERNAME_DB_KEY);
var storeRealname = self._appConfigStore.storeList([self.realname], REALNAME_DB_KEY);
var storePassword = new Promise(function(resolve) {
if (!self.password) {
resolve();
return;
}
if (self.passwordNeedsDecryption) {
// password is not decrypted yet, so no need to re-encrypt it before storing...
return self._appConfigStore.storeList([self.password], PASSWD_DB_KEY).then(resolve);
}
return self._pgp.encrypt(self.password, undefined).then(function(ciphertext) {
return self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY).then(resolve);
});
});
return Promise.all([
storeSmtp,
storeImap,
storeEmailAddress,
storeUsername,
storeRealname,
storePassword
]).then(function() {
self.credentialsDirty = false;
});
};
/**
* Returns the email address. Loads it from disk, if necessary
*/
Auth.prototype.getEmailAddress = function() {
var self = this;
if (self.emailAddress) {
return new Promise(function(resolve) {
resolve({
emailAddress: self.emailAddress,
realname: self.realname
});
});
}
return self._loadCredentials().then(function() {
return {
emailAddress: self.emailAddress,
realname: self.realname
};
});
};
/**
* Check if the current platform and mail provider support OAuth.
* @param {String} hostname The hostname of the mail server e.g. imap.gmail.com
* @return {Boolean} If oauth should be used
*/
Auth.prototype.useOAuth = function(hostname) {
if (!this._oauth.isSupported()) {
return false;
}
var regex = cfg.oauthDomains;
for (var i = 0; i < regex.length; i++) {
if (regex[i].test(hostname)) {
return true;
}
}
return false;
};
/**
* READ FIRST b/c usage of the oauth api is weird.
* the chrome identity api will let you query an oauth token for an email account without knowing
* the corresponding email address. also, android has multiple accounts whereas desktop chrome only
* has one user logged in.
* 1) try to read the email address from the configuration (see above)
* 2) fetch the oauth token. if we already HAVE an email address at this point, we can spare
* popping up the account picker on android! if not, the account picker will pop up. this
* is android only, since the desktop chrome will query the user that is logged into chrome
* 3) fetch the email address for the oauth token from the chrome identity api
*/
Auth.prototype.getOAuthToken = function() {
var self = this;
// get a fresh oauth token
return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
function onToken(oauthToken) {
// shortcut if the email address is already known
if (self.emailAddress) {
self.oauthToken = oauthToken;
return;
}
// query the email address
return self._oauth.queryEmailAddress(oauthToken).then(function(emailAddress) {
self.oauthToken = oauthToken;
self.emailAddress = emailAddress;
});
}
};
/**
* Loads email address, password, ... from disk and sets them on `this`
*/
Auth.prototype._loadCredentials = function() {
var self = this;
if (self.initialized) {
return new Promise(function(resolve) {
resolve();
});
}
return loadFromDB(SMTP_DB_KEY).then(function(smtp) {
self.smtp = smtp;
return loadFromDB(IMAP_DB_KEY);
}).then(function(imap) {
self.imap = imap;
return loadFromDB(USERNAME_DB_KEY);
}).then(function(username) {
self.username = username;
return loadFromDB(REALNAME_DB_KEY);
}).then(function(realname) {
self.realname = realname;
return loadFromDB(EMAIL_ADDR_DB_KEY);
}).then(function(emailAddress) {
self.emailAddress = emailAddress;
return loadFromDB(PASSWD_DB_KEY);
}).then(function(password) {
self.password = password;
self.passwordNeedsDecryption = !!password;
self.initialized = true;
});
function loadFromDB(key) {
return self._appConfigStore.listItems(key, 0, null).then(function(cachedItems) {
return cachedItems && cachedItems[0];
});
}
};
/**
* Handles certificate updates and errors by notifying the user.
* @param {String} component Either imap or smtp
* @param {Function} callback The error handler
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate
*/
Auth.prototype.handleCertificateUpdate = function(component, reconnectCallback, callback, pemEncodedCert) {
var self = this;
axe.debug('new ssl certificate received: ' + pemEncodedCert);
if (!self[component].ca) {
// no previous ssl cert, trust on first use
self[component].ca = pemEncodedCert;
self.credentialsDirty = true;
self.storeCredentials().then(callback).catch(callback);
return;
}
if (self[component].ca === pemEncodedCert) {
// ignore multiple successive tls handshakes, e.g. for gmail
return;
}
// previous ssl cert known, does not match: query user and certificate
callback({
title: str.updateCertificateTitle,
message: str.updateCertificateMessage.replace('{0}', self[component].host),
positiveBtnStr: str.updateCertificatePosBtn,
negativeBtnStr: str.updateCertificateNegBtn,
showNegativeBtn: true,
faqLink: str.certificateFaqLink,
callback: function(granted) {
if (!granted) {
return;
}
self[component].ca = pemEncodedCert;
self.credentialsDirty = true;
self.storeCredentials().then(function() {
reconnectCallback(callback);
}).catch(callback);
}
});
};
/**
* Logout of the app by clearing the app config store and in memory credentials
*/
Auth.prototype.logout = function() {
var self = this;
// clear app config db
return self._appConfigStore.clear().then(function() {
// clear in memory cache
self.setCredentials({});
self.initialized = undefined;
self.credentialsDirty = undefined;
self.passwordNeedsDecryption = undefined;
});
};