2014-10-02 16:05:44 -04:00
|
|
|
'use strict';
|
|
|
|
|
2014-11-18 14:19:29 -05:00
|
|
|
var ngModule = angular.module('woServices');
|
|
|
|
ngModule.service('auth', Auth);
|
|
|
|
module.exports = Auth;
|
|
|
|
|
2014-10-02 16:05:44 -04:00
|
|
|
var axe = require('axe-logger'),
|
2014-11-11 13:30:58 -05:00
|
|
|
cfg = require('../app-config').config,
|
2014-10-02 16:05:44 -04:00
|
|
|
str = require('../app-config').string;
|
|
|
|
|
2014-12-01 08:55:03 -05:00
|
|
|
var APP_CONFIG_DB_NAME = 'app-config';
|
2014-10-02 16:05:44 -04:00
|
|
|
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
|
2014-11-11 13:30:58 -05:00
|
|
|
* auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP,
|
2014-10-02 16:05:44 -04:00
|
|
|
* username, password / oauth token, IMAP/SMTP server host names, ...
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
function Auth(appConfigStore, oauth, pgp) {
|
2014-11-19 14:54:59 -05:00
|
|
|
this._appConfigStore = appConfigStore;
|
2014-10-02 16:05:44 -04:00
|
|
|
this._oauth = oauth;
|
|
|
|
this._pgp = pgp;
|
2014-12-11 14:27:55 -05:00
|
|
|
|
|
|
|
this._initialized = false;
|
2014-11-18 14:19:29 -05:00
|
|
|
}
|
2014-10-02 16:05:44 -04:00
|
|
|
|
2014-11-19 14:54:59 -05:00
|
|
|
/**
|
|
|
|
* Initialize the service
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.init = function() {
|
2014-12-01 08:55:03 -05:00
|
|
|
var self = this;
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._appConfigStore.init(APP_CONFIG_DB_NAME).then(function() {
|
|
|
|
self._initialized = true;
|
2014-12-01 08:55:03 -05:00
|
|
|
});
|
2014-11-19 14:54:59 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the service has been initialized.
|
|
|
|
*/
|
|
|
|
Auth.prototype.isInitialized = function() {
|
|
|
|
return this._initialized;
|
|
|
|
};
|
|
|
|
|
2014-10-02 16:05:44 -04:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.getCredentials = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
|
|
|
|
2014-11-11 13:30:58 -05:00
|
|
|
if (!self.emailAddress) {
|
2014-10-02 16:05:44 -04:00
|
|
|
// we're not yet initialized, so let's load our stuff from disk
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._loadCredentials().then(chooseLogin);
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
return chooseLogin();
|
2014-10-02 16:05:44 -04:00
|
|
|
|
|
|
|
function chooseLogin() {
|
2014-11-11 13:30:58 -05:00
|
|
|
if (self.useOAuth(self.imap.host) && !self.password) {
|
|
|
|
// oauth login
|
2014-12-11 14:27:55 -05:00
|
|
|
return self.getOAuthToken().then(done);
|
2014-07-01 13:49:19 -04:00
|
|
|
}
|
|
|
|
|
2014-10-02 16:05:44 -04:00
|
|
|
if (self.passwordNeedsDecryption) {
|
|
|
|
// decrypt password
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._pgp.decrypt(self.password, undefined).then(function(cleartext) {
|
2014-10-02 16:05:44 -04:00
|
|
|
self.passwordNeedsDecryption = false;
|
|
|
|
self.password = cleartext;
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(done);
|
2014-07-01 13:49:19 -04:00
|
|
|
}
|
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
return done();
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function done() {
|
2014-12-11 14:27:55 -05:00
|
|
|
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
|
|
|
|
}
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
2014-12-11 14:27:55 -05:00
|
|
|
};
|
|
|
|
resolve(credentials);
|
|
|
|
});
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
2014-11-11 13:30:58 -05:00
|
|
|
this.smtp = options.smtp; // host, port, secure, ca
|
|
|
|
this.imap = options.imap; // host, port, secure, ca
|
2014-10-02 16:05:44 -04:00
|
|
|
};
|
|
|
|
|
2014-12-10 12:28:01 -05:00
|
|
|
Auth.prototype.storeCredentials = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
2014-12-11 14:27:55 -05:00
|
|
|
|
|
|
|
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) {
|
2014-12-10 12:28:01 -05:00
|
|
|
resolve();
|
|
|
|
return;
|
2014-07-01 13:49:19 -04:00
|
|
|
}
|
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
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);
|
2014-04-01 07:16:39 -04:00
|
|
|
});
|
2014-12-11 14:27:55 -05:00
|
|
|
});
|
2014-12-10 12:28:01 -05:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
return Promise.all([
|
|
|
|
storeSmtp,
|
|
|
|
storeImap,
|
|
|
|
storeEmailAddress,
|
|
|
|
storeUsername,
|
|
|
|
storeRealname,
|
|
|
|
storePassword
|
|
|
|
]).then(function() {
|
2014-12-10 12:28:01 -05:00
|
|
|
self.credentialsDirty = false;
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the email address. Loads it from disk, if necessary
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.getEmailAddress = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if (self.emailAddress) {
|
2014-12-11 14:27:55 -05:00
|
|
|
return new Promise(function(resolve) {
|
|
|
|
resolve({
|
|
|
|
emailAddress: self.emailAddress,
|
|
|
|
realname: self.realname
|
|
|
|
});
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
|
|
|
}
|
2014-07-01 13:49:19 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._loadCredentials().then(function() {
|
|
|
|
return {
|
2014-10-02 16:05:44 -04:00
|
|
|
emailAddress: self.emailAddress,
|
|
|
|
realname: self.realname
|
2014-12-11 14:27:55 -05:00
|
|
|
};
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-11-11 13:30:58 -05:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
|
|
|
|
2014-10-02 16:05:44 -04:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.getOAuthToken = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if (self.oauthToken) {
|
|
|
|
// removed cached token and get a new one
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._oauth.refreshToken({
|
2014-10-02 16:05:44 -04:00
|
|
|
emailAddress: self.emailAddress,
|
|
|
|
oldToken: self.oauthToken
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(onToken);
|
2014-10-02 16:05:44 -04:00
|
|
|
} else {
|
|
|
|
// get a fresh oauth token
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
function onToken(oauthToken) {
|
2014-10-02 16:05:44 -04:00
|
|
|
// shortcut if the email address is already known
|
|
|
|
if (self.emailAddress) {
|
|
|
|
self.oauthToken = oauthToken;
|
2014-12-11 14:27:55 -05:00
|
|
|
return;
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// query the email address
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._oauth.queryEmailAddress(oauthToken).then(function(emailAddress) {
|
2014-10-02 16:05:44 -04:00
|
|
|
self.oauthToken = oauthToken;
|
|
|
|
self.emailAddress = emailAddress;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2014-11-11 13:30:58 -05:00
|
|
|
* Loads email address, password, ... from disk and sets them on `this`
|
2014-10-02 16:05:44 -04:00
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype._loadCredentials = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if (self.initialized) {
|
2014-12-11 14:27:55 -05:00
|
|
|
return new Promise(function(resolve) {
|
|
|
|
resolve();
|
|
|
|
});
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
return loadFromDB(SMTP_DB_KEY).then(function(smtp) {
|
|
|
|
self.smtp = smtp;
|
|
|
|
return loadFromDB(IMAP_DB_KEY);
|
2014-07-01 13:49:19 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(function(imap) {
|
|
|
|
self.imap = imap;
|
|
|
|
return loadFromDB(USERNAME_DB_KEY);
|
2014-10-02 16:05:44 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(function(username) {
|
|
|
|
self.username = username;
|
|
|
|
return loadFromDB(REALNAME_DB_KEY);
|
2014-04-01 07:16:39 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(function(realname) {
|
|
|
|
self.realname = realname;
|
|
|
|
return loadFromDB(EMAIL_ADDR_DB_KEY);
|
2014-07-01 13:49:19 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(function(emailAddress) {
|
|
|
|
self.emailAddress = emailAddress;
|
|
|
|
return loadFromDB(PASSWD_DB_KEY);
|
2014-07-01 13:49:19 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
}).then(function(password) {
|
|
|
|
self.password = password;
|
|
|
|
self.passwordNeedsDecryption = !!password;
|
|
|
|
self.initialized = true;
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
2014-07-01 13:49:19 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
function loadFromDB(key) {
|
|
|
|
return self._appConfigStore.listItems(key, 0, null).then(function(cachedItems) {
|
|
|
|
return cachedItems && cachedItems[0];
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles certificate updates and errors by notifying the user.
|
|
|
|
* @param {String} component Either imap or smtp
|
|
|
|
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.handleCertificateUpdate = function(component, onConnect, pemEncodedCert) {
|
2014-10-02 16:05:44 -04:00
|
|
|
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;
|
2014-12-11 14:27:55 -05:00
|
|
|
return self.storeCredentials();
|
2014-10-02 16:05:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2014-12-11 14:27:55 -05:00
|
|
|
return new Promise(function() {
|
|
|
|
throw {
|
|
|
|
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) {
|
2014-10-02 16:05:44 -04:00
|
|
|
return;
|
|
|
|
}
|
2014-04-01 07:16:39 -04:00
|
|
|
|
2014-12-11 14:27:55 -05:00
|
|
|
self[component].ca = pemEncodedCert;
|
|
|
|
self.credentialsDirty = true;
|
|
|
|
return self.storeCredentials().then(function() {
|
|
|
|
return onConnect();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2014-10-02 16:05:44 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logout of the app by clearing the app config store and in memory credentials
|
|
|
|
*/
|
2014-12-11 14:27:55 -05:00
|
|
|
Auth.prototype.logout = function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
// clear app config db
|
2014-12-11 14:27:55 -05:00
|
|
|
return self._appConfigStore.clear().then(function() {
|
2014-10-02 16:05:44 -04:00
|
|
|
// clear in memory cache
|
|
|
|
self.setCredentials({});
|
|
|
|
self.initialized = undefined;
|
|
|
|
self.credentialsDirty = undefined;
|
|
|
|
self.passwordNeedsDecryption = undefined;
|
|
|
|
});
|
2014-11-18 14:19:29 -05:00
|
|
|
};
|