Bug 1155491 - Support autoconfig and manual config of gmail IMAP OAuth2 authentication, r=jcranmer

This commit is contained in:
R Kent James 2015-04-22 22:35:17 -07:00
Родитель 05f8ea8aa3
Коммит 6423d3829a
13 изменённых файлов: 220 добавлений и 41 удалений

Просмотреть файл

@ -50,6 +50,8 @@ AccountConfig.prototype =
*/
incomingAlternatives : null,
outgoingAlternatives : null,
// OAuth2 configuration, if needed.
oauthSettings : null,
// just an internal string to refer to this. Do not show to user.
id : null,
// who created the config.

Просмотреть файл

@ -28,6 +28,11 @@ function createAccountInBackend(config)
if (config.rememberPassword && config.incoming.password.length)
rememberPassword(inServer, config.incoming.password);
if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
}
// SSL
if (config.incoming.socketType == 1) // plain
inServer.socketType = Ci.nsMsgSocketType.plain;
@ -102,6 +107,14 @@ function createAccountInBackend(config)
rememberPassword(outServer, config.incoming.password);
}
if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
let pref = "mail.smtpserver." + outServer.key + ".";
Services.prefs.setCharPref(pref + "oauth2.scope",
config.oauthSettings.scope);
Services.prefs.setCharPref(pref + "oauth2.issuer",
config.oauthSettings.issuer);
}
if (config.outgoing.socketType == 1) // no SSL
outServer.socketType = Ci.nsMsgSocketType.plain;
else if (config.outgoing.socketType == 2) // SSL / TLS

Просмотреть файл

@ -6,6 +6,7 @@
Components.utils.import("resource:///modules/mailServices.js");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource:///modules/hostnameUtils.jsm");
Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
/**
* This is the dialog opened by menu File | New account | Mail... .
@ -34,8 +35,10 @@ Components.utils.import("resource:///modules/hostnameUtils.jsm");
// from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html
var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i;
Cu.import("resource:///modules/gloda/log4moz.js");
let gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
if (typeof gEmailWizardLogger == "undefined") {
Cu.import("resource:///modules/gloda/log4moz.js");
var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
}
var gStringsBundle;
var gMessengerBundle;
@ -181,6 +184,7 @@ EmailConfigWizard.prototype =
"authPasswordEncrypted");
setLabelFromStringBundle("in-authMethod-kerberos", "authKerberos");
setLabelFromStringBundle("in-authMethod-ntlm", "authNTLM");
setLabelFromStringBundle("in-authMethod-oauth2", "authOAuth2");
setLabelFromStringBundle("out-authMethod-no", "authNo");
setLabelFromStringBundle("out-authMethod-password-cleartext",
"authPasswordCleartextViaSSL"); // will warn about insecure later
@ -188,6 +192,7 @@ EmailConfigWizard.prototype =
"authPasswordEncrypted");
setLabelFromStringBundle("out-authMethod-kerberos", "authKerberos");
setLabelFromStringBundle("out-authMethod-ntlm", "authNTLM");
setLabelFromStringBundle("out-authMethod-oauth2", "authOAuth2");
e("incoming_port").value = gStringsBundle.getString("port_auto");
this.fillPortDropdown("smtp");
@ -665,7 +670,7 @@ EmailConfigWizard.prototype =
assert(config instanceof AccountConfig,
"BUG: Arg 'config' needs to be an AccountConfig object");
this._haveValidConfigForDomain = this._email.split("@")[1];;
this._haveValidConfigForDomain = this._email.split("@")[1];
if (!this._realname || !this._email) {
return;
@ -792,6 +797,7 @@ EmailConfigWizard.prototype =
unknownString);
let certStatus = gStringsBundle.getString(server.badCert ?
"resultSSLCertWeak" : "resultSSLCertOK");
// TODO: we should really also display authentication method here.
return gStringsBundle.getFormattedString(stringName,
[ type, host, ssl, certStatus ]);
};
@ -1008,7 +1014,7 @@ EmailConfigWizard.prototype =
e("incoming_ssl").value = sanitize.enum(config.incoming.socketType,
[ 0, 1, 2, 3 ], 0);
e("incoming_authMethod").value = sanitize.enum(config.incoming.auth,
[ 0, 3, 4, 5, 6 ], 0);
[ 0, 3, 4, 5, 6, 10 ], 0);
e("incoming_username").value = config.incoming.username;
if (config.incoming.port) {
e("incoming_port").value = config.incoming.port;
@ -1017,6 +1023,19 @@ EmailConfigWizard.prototype =
}
this.fillPortDropdown(config.incoming.type);
// If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname);
gEmailWizardLogger.info("OAuth2 details for incoming hostname " +
config.incoming.hostname + " is " + iDetails);
e("in-authMethod-oauth2").hidden = !(iDetails && e("incoming_protocol").value == 1);
if (!e("in-authMethod-oauth2").hidden) {
config.oauthSettings = {};
[config.oauthSettings.issuer, config.oauthSettings.scope] = iDetails;
// oauthsettings are not stored nor changable in the user interface, so just
// store them in the base configuration.
this._currentConfig.oauthSettings = config.oauthSettings;
}
// outgoing server
e("outgoing_hostname").value = config.outgoing.hostname;
e("outgoing_username").value = config.outgoing.username;
@ -1026,14 +1045,27 @@ EmailConfigWizard.prototype =
e("outgoing_ssl").value = sanitize.enum(config.outgoing.socketType,
[ 0, 1, 2, 3 ], 0);
e("outgoing_authMethod").value = sanitize.enum(config.outgoing.auth,
[ 0, 1, 3, 4, 5, 6 ], 0);
[ 0, 1, 3, 4, 5, 6, 10 ], 0);
if (config.outgoing.port) {
e("outgoing_port").value = config.outgoing.port;
} else {
this.adjustOutgoingPortToSSLAndProtocol(config);
}
// populate fields even if existingServerKey, in case user changes back
// If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname);
gEmailWizardLogger.info("OAuth2 details for outgoing hostname " +
config.outgoing.hostname + " is " + oDetails);
e("out-authMethod-oauth2").hidden = !oDetails;
if (!e("out-authMethod-oauth2").hidden) {
config.oauthSettings = {};
[config.oauthSettings.issuer, config.oauthSettings.scope] = oDetails;
// oauthsettings are not stored nor changable in the user interface, so just
// store them in the base configuration.
this._currentConfig.oauthSettings = config.oauthSettings;
}
// populate fields even if existingServerKey, in case user changes back
if (config.outgoing.existingServerKey) {
let menulist = e("outgoing_hostname");
// We can't use menulist.value = config.outgoing.existingServerKey
@ -1581,6 +1613,12 @@ EmailConfigWizard.prototype =
self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth;
self._currentConfig.incoming.username = successfulConfig.incoming.username;
self._currentConfig.outgoing.username = successfulConfig.outgoing.username;
// We loaded dynamic client registration, fill this data back in to the
// config set.
if (successfulConfig.oauthSettings)
self._currentConfig.oauthSettings = successfulConfig.oauthSettings;
self.finish();
},
function(e) // failed

Просмотреть файл

@ -307,6 +307,7 @@
<menuitem id="in-authMethod-password-encrypted" value="4"/>
<menuitem id="in-authMethod-kerberos" value="5"/>
<menuitem id="in-authMethod-ntlm" value="6"/>
<menuitem id="in-authMethod-oauth2" value="10" hidden="true"/>
</menupopup>
</menulist>
</row>
@ -356,6 +357,7 @@
<menuitem id="out-authMethod-password-encrypted" value="4"/>
<menuitem id="out-authMethod-kerberos" value="5"/>
<menuitem id="out-authMethod-ntlm" value="6"/>
<menuitem id="out-authMethod-oauth2" value="10" hidden="true"/>
</menupopup>
</menulist>
</row>

Просмотреть файл

@ -59,6 +59,13 @@ function guessConfig(domain, progressCallback, successCallback, errorCallback,
assert(typeof(progressCallback) == "function", "need progressCallback");
assert(typeof(successCallback) == "function", "need successCallback");
assert(typeof(errorCallback) == "function", "need errorCallback");
// Servers that we know enough that they support OAuth2 do not need guessing.
if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) {
successCallback(resultConfig);
return null;
}
if (!resultConfig)
resultConfig = new AccountConfig();
resultConfig.source = AccountConfig.kSourceGuess;

Просмотреть файл

@ -54,6 +54,16 @@ function readFromXML(clientConfigXML)
throw exception ? exception : "need proper <domain> in XML";
exception = null;
let oauthSettings = null;
if ("oauth2Settings" in clientConfigXML.clientConfig) {
oauthSettings = {};
oauthSettings.scope = sanitize.nonemptystring(
clientConfigXML.clientConfig.oauth2Settings.scope);
if (!oauthSettings.scope)
throw new Error("Malformed oauth2Settings in configuration XML");
d.oauthSettings = oauthSettings;
}
// incoming server
for (let iX of array_or_undef(xml.$incomingServer)) // input (XML)
{
@ -94,7 +104,13 @@ function readFromXML(clientConfigXML)
// @deprecated TODO remove
"secure" : Ci.nsMsgAuthMethod.passwordEncrypted,
"GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI,
"NTLM" : Ci.nsMsgAuthMethod.NTLM });
"NTLM" : Ci.nsMsgAuthMethod.NTLM,
"OAuth2" : Ci.nsMsgAuthMethod.OAuth2 });
// If we're using OAuth2, but don't have working settings, bail.
if (iO.auth == Ci.nsMsgAuthMethod.OAuth2 && !oauthSettings)
continue;
break; // take first that we support
} catch (e) { exception = e; }
}
@ -175,7 +191,13 @@ function readFromXML(clientConfigXML)
"secure" : Ci.nsMsgAuthMethod.passwordEncrypted,
"GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI,
"NTLM" : Ci.nsMsgAuthMethod.NTLM,
"OAuth2" : Ci.nsMsgAuthMethod.OAuth2,
});
// If we're using OAuth2, but don't have working settings, bail.
if (oO.auth == Ci.nsMsgAuthMethod.OAuth2 && !oauthSettings)
continue;
break; // take first that we support
} catch (e) { exception = e; }
}

Просмотреть файл

@ -118,7 +118,7 @@ var sanitize =
var uri;
try {
uri = ioService().newURI(str, null, null);
uri = Services.io.newURI(str, null, null);
uri = uri.QueryInterface(Ci.nsIURL);
} catch (e) {
throw new MalformedException("url_parsing.error", unchecked);

Просмотреть файл

@ -248,11 +248,13 @@ function deepCopy(org)
return result;
}
let kDebug = false;
if (typeof gEmailWizardLogger == "undefined") {
Cu.import("resource:///modules/gloda/log4moz.js");
var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
}
function ddump(text)
{
if (kDebug)
dump(text + "\n");
gEmailWizardLogger.info(text);
}
function debugObject(obj, name, maxDepth, curDepth)

Просмотреть файл

@ -30,6 +30,12 @@
*/
Components.utils.import("resource:///modules/mailServices.js");
Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
if (typeof gEmailWizardLogger == "undefined") {
Cu.import("resource:///modules/gloda/log4moz.js");
var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
}
function verifyConfig(config, alter, msgWindow, successCallback, errorCallback)
{
@ -63,10 +69,35 @@ function verifyConfig(config, alter, msgWindow, successCallback, errorCallback)
inServer.socketType = Ci.nsMsgSocketType.SSL;
else if (config.incoming.socketType == 3) // STARTTLS
inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
gEmailWizardLogger.info("Setting incoming server authMethod to " +
config.incoming.auth);
inServer.authMethod = config.incoming.auth;
try {
if (inServer.password)
// Lookup issuer if needed.
if (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 ||
config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2) {
if (!config.oauthSettings.issuer || !config.oauthSettings.scope) {
// lookup issuer or scope from hostname
let hostname = (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) ?
config.incoming.hostname : config.outgoing.hostname;
let hostDetails = OAuth2Providers.getHostnameDetails(hostname);
if (hostDetails)
[config.oauthSettings.issuer, config.oauthSettings.scope] = hostDetails;
if (!config.oauthSettings.issuer || !config.oauthSettings.scope)
throw "Could not get issuer for oauth2 authentication";
}
gEmailWizardLogger.info("Saving oauth parameters for issuer " +
config.oauthSettings.issuer);
inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
gEmailWizardLogger.info("OAuth2 issuer, scope is " +
config.oauthSettings.issuer + ", " + config.oauthSettings.scope);
}
if (inServer.password ||
inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2)
verifyLogon(config, inServer, alter, msgWindow,
successCallback, errorCallback);
else {
@ -74,16 +105,20 @@ function verifyConfig(config, alter, msgWindow, successCallback, errorCallback)
MailServices.accounts.removeIncomingServer(inServer, true);
successCallback(config);
}
} catch (e) {
ddump("ERROR: verify logon shouldn't have failed");
errorCallback(e);
throw(e);
return;
}
};
catch (e) {
gEmailWizardLogger.error("ERROR: verify logon shouldn't have failed");
}
// Avoid pref pollution, clear out server prefs.
MailServices.accounts.removeIncomingServer(inServer, true);
errorCallback(e);
}
function verifyLogon(config, inServer, alter, msgWindow, successCallback,
errorCallback)
{
gEmailWizardLogger.info("verifyLogon for server at " + inServer.hostName);
// hack - save away the old callbacks.
let saveCallbacks = msgWindow.notificationCallbacks;
// set our own callbacks - this works because verifyLogon will
@ -99,6 +134,7 @@ function verifyLogon(config, inServer, alter, msgWindow, successCallback,
// clear msgWindow so url won't prompt for passwords.
uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null;
}
catch (e) { gEmailWizardLogger.error("verifyLogon failed: " + e); throw e;}
finally {
// restore them
msgWindow.notificationCallbacks = saveCallbacks;

Просмотреть файл

@ -9,6 +9,7 @@ const Ci = Components.interfaces;
const Cr = Components.results;
Components.utils.import("resource://gre/modules/OAuth2.jsm");
Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
Components.utils.import("resource://gre/modules/Preferences.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
@ -22,19 +23,11 @@ OAuth2Module.prototype = {
classID: Components.ID("{b63d8e4c-bf60-439b-be0e-7c9f67291042}"),
_loadOAuthClientDetails(aIssuer) {
if (aIssuer == "accounts.google.com") {
// For the moment, these details are hard-coded, since Google does not
// provide dynamic client registration. Don't copy these values for your
// own application--register it yourself. This code (and possibly even the
// registration itself) will disappear when this is switched to dynamic
// client registration.
this._appKey = '406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com';
this._appSecret = 'kSmqreRr0qwBWJgbf5Y-PjSU';
this._authURI = "https://accounts.google.com/o/oauth2/auth";
this._tokenURI = "https://www.googleapis.com/oauth2/v3/token";
} else {
let details = OAuth2Providers.getIssuerDetails(aIssuer);
if (details)
[this._appKey, this._appSecret, this._authURI, this._tokenURI] = details;
else
throw Cr.NS_ERROR_INVALID_ARGUMENT;
}
},
initFromSmtp(aServer) {
return this._initPrefs("mail.smtpserver." + aServer.key + ".",
@ -53,10 +46,10 @@ OAuth2Module.prototype = {
// have them, we don't support OAuth2.
if (!issuer || !scope) {
// Since we currently only support gmail, init values if server matches.
if (aHostname == "imap.googlemail.com" || aHostname == "smtp.googlemail.com")
let details = OAuth2Providers.getHostnameDetails(aHostname);
if (details)
{
issuer = "accounts.google.com";
scope = "http://mail.google.com/";
[issuer, scope] = details;
Preferences.set(root + "oauth2.issuer", issuer);
Preferences.set(root + "oauth2.scope", scope);
}

Просмотреть файл

@ -56,10 +56,6 @@ OAuth2.prototype = {
tokenExpires: 0,
connect: function connect(aSuccess, aFailure, aWithUI, aRefresh) {
if (gConnecting[this.authURI]) {
aFailure("Window already open");
return;
}
this.connectSuccessCallback = aSuccess;
this.connectFailureCallback = aFailure;
@ -67,14 +63,16 @@ OAuth2.prototype = {
if (!aRefresh && this.accessToken) {
aSuccess();
} else if (this.refreshToken) {
gConnecting[this.authURI] = true;
this.requestAccessToken(this.refreshToken, OAuth2.CODE_REFRESH);
} else {
if (!aWithUI) {
aFailure('{ "error": "auth_noui" }');
return;
}
gConnecting[this.authURI] = true;
if (gConnecting[this.authURI]) {
aFailure("Window already open");
return;
}
this.requestAuthorization();
}
},
@ -156,9 +154,11 @@ OAuth2.prototype = {
};
this.wrappedJSObject = this._browserRequest;
gConnecting[this.authURI] = true;
Services.ww.openWindow(null, this.requestWindowURI, null, this.requestWindowFeatures, this);
},
finishAuthorizationRequest: function() {
gConnecting[this.authURI] = false;
if (!("_browserRequest" in this)) {
return;
}
@ -183,7 +183,6 @@ OAuth2.prototype = {
},
onAuthorizationFailed: function(aError, aData) {
gConnecting[this.authURI] = false;
this.connectFailureCallback(aData);
},
@ -210,7 +209,6 @@ OAuth2.prototype = {
},
onAccessTokenFailed: function onAccessTokenFailed(aError, aData) {
gConnecting[this.authURI] = false;
if (aError != "offline") {
this.refreshToken = null;
}
@ -218,7 +216,6 @@ OAuth2.prototype = {
},
onAccessTokenReceived: function onRequestTokenReceived(aData) {
gConnecting[this.authURI] = false;
let result = JSON.parse(aData);
this.accessToken = result.access_token;

Просмотреть файл

@ -0,0 +1,66 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Details of supported OAuth2 Providers.
*/
var EXPORTED_SYMBOLS = ["OAuth2Providers"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
// map of hostnames to [issuer, scope]
const kHostnames = new Map([
["imap.googlemail.com", ["accounts.google.com", "http://mail.google.com/"]],
["smtp.googlemail.com", ["accounts.google.com", "http://mail.google.com/"]],
]);
// map of issuers to appKey, appSecret, authURI, tokenURI
// For the moment, these details are hard-coded, since Google does not
// provide dynamic client registration. Don't copy these values for your
// own application--register it yourself. This code (and possibly even the
// registration itself) will disappear when this is switched to dynamic
// client registration.
const kIssuers = new Map ([
["accounts.google.com", [
'406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com',
'kSmqreRr0qwBWJgbf5Y-PjSU',
'https://accounts.google.com/o/oauth2/auth',
'https://www.googleapis.com/oauth2/v3/token'
]],
]);
/**
* OAuth2Providers: Methods to lookup OAuth2 parameters for supported
* email providers.
*/
var OAuth2Providers = {
/**
* Map a hostname to the relevant issuer and scope.
*
* @param aHostname String representing the url for an imap or smtp
* server (example "imap.googlemail.com").
*
* @returns Array with [issuer, scope] for the hostname if found,
* else undefined. issuer is a string representing the
* organization, scope is an oauth parameter describing\
* the required access level.
*/
getHostnameDetails: function (aHostname) { return kHostnames.get(aHostname);},
/**
* Map an issuer to OAuth2 account details.
*
* @param aIssuer The organization issuing oauth2 parameters, example
* "accounts.google.com".
*
* @return Array containing [appKey, appSecret, authURI, tokenURI]
* where appKey and appDetails are strings representing the
* account registered for Thunderbird with the organization,
* authURI and tokenURI are url strings representing
* endpoints to access OAuth2 authentication.
*/
getIssuerDetails: function (aIssuer) { return kIssuers.get(aIssuer);}
}

Просмотреть файл

@ -60,6 +60,7 @@ EXTRA_JS_MODULES += [
'mailServices.js',
'msgDBCacheManager.js',
'OAuth2.jsm',
'OAuth2Providers.jsm',
'StringBundle.js',
'templateUtils.js',
'traceHelper.js',