mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
6135d646ca
MozReview-Commit-ID: 1xb6sWwhjvd --HG-- extra : rebase_source : b29de9b4d966a824aee0c65c838e79bc9f35837c
310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["IdentityService"];
|
|
|
|
const Cu = Components.utils;
|
|
const Ci = Components.interfaces;
|
|
const Cc = Components.classes;
|
|
const Cr = Components.results;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/identity/LogUtils.jsm");
|
|
Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
|
|
Cu.import("resource://gre/modules/identity/RelyingParty.jsm");
|
|
Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this,
|
|
"jwcrypto",
|
|
"resource://gre/modules/identity/jwcrypto.jsm");
|
|
|
|
function log(...aMessageArgs) {
|
|
Logger.log.apply(Logger, ["core"].concat(aMessageArgs));
|
|
}
|
|
function reportError(...aMessageArgs) {
|
|
Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
|
|
}
|
|
|
|
function IDService() {
|
|
Services.obs.addObserver(this, "quit-application-granted", false);
|
|
Services.obs.addObserver(this, "identity-auth-complete", false);
|
|
|
|
this._store = IdentityStore;
|
|
this.RP = RelyingParty;
|
|
this.IDP = IdentityProvider;
|
|
}
|
|
|
|
IDService.prototype = {
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
|
|
|
|
observe: function observe(aSubject, aTopic, aData) {
|
|
switch (aTopic) {
|
|
case "quit-application-granted":
|
|
Services.obs.removeObserver(this, "quit-application-granted");
|
|
this.shutdown();
|
|
break;
|
|
case "identity-auth-complete":
|
|
if (!aSubject || !aSubject.wrappedJSObject)
|
|
break;
|
|
let subject = aSubject.wrappedJSObject;
|
|
log("Auth complete:", aSubject.wrappedJSObject);
|
|
// We have authenticated in order to provision an identity.
|
|
// So try again.
|
|
this.selectIdentity(subject.rpId, subject.identity);
|
|
break;
|
|
}
|
|
},
|
|
|
|
reset: function reset() {
|
|
// Explicitly call reset() on our RP and IDP classes.
|
|
// This is here to make testing easier. When the
|
|
// quit-application-granted signal is emitted, reset() will be
|
|
// called here, on RP, on IDP, and on the store. So you don't
|
|
// need to use this :)
|
|
this._store.reset();
|
|
this.RP.reset();
|
|
this.IDP.reset();
|
|
},
|
|
|
|
shutdown: function shutdown() {
|
|
log("shutdown");
|
|
Services.obs.removeObserver(this, "identity-auth-complete");
|
|
// try to prevent abort/crash during shutdown of mochitest-browser2...
|
|
try {
|
|
Services.obs.removeObserver(this, "quit-application-granted");
|
|
} catch (e) {}
|
|
},
|
|
|
|
/**
|
|
* Parse an email into username and domain if it is valid, else return null
|
|
*/
|
|
parseEmail: function parseEmail(email) {
|
|
var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
|
|
if (match) {
|
|
return {
|
|
username: match[1],
|
|
domain: match[2]
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* The UX wants to add a new identity
|
|
* often followed by selectIdentity()
|
|
*
|
|
* @param aIdentity
|
|
* (string) the email chosen for login
|
|
*/
|
|
addIdentity: function addIdentity(aIdentity) {
|
|
if (this._store.fetchIdentity(aIdentity) === null) {
|
|
this._store.addIdentity(aIdentity, null, null);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The UX comes back and calls selectIdentity once the user has picked
|
|
* an identity.
|
|
*
|
|
* @param aRPId
|
|
* (integer) the id of the doc object obtained in .watch() and
|
|
* passed to the UX component.
|
|
*
|
|
* @param aIdentity
|
|
* (string) the email chosen for login
|
|
*/
|
|
selectIdentity: function selectIdentity(aRPId, aIdentity) {
|
|
log("selectIdentity: RP id:", aRPId, "identity:", aIdentity);
|
|
|
|
// Get the RP that was stored when watch() was invoked.
|
|
let rp = this.RP._rpFlows[aRPId];
|
|
if (!rp) {
|
|
reportError("selectIdentity", "Invalid RP id: ", aRPId);
|
|
return;
|
|
}
|
|
|
|
// It's possible that we are in the process of provisioning an
|
|
// identity.
|
|
let provId = rp.provId;
|
|
|
|
let rpLoginOptions = {
|
|
loggedInUser: aIdentity,
|
|
origin: rp.origin
|
|
};
|
|
log("selectIdentity: provId:", provId, "origin:", rp.origin);
|
|
|
|
// Once we have a cert, and once the user is authenticated with the
|
|
// IdP, we can generate an assertion and deliver it to the doc.
|
|
let self = this;
|
|
this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) {
|
|
if (!err && assertion) {
|
|
self.RP._doLogin(rp, rpLoginOptions, assertion);
|
|
return;
|
|
|
|
}
|
|
// Need to provision an identity first. Begin by discovering
|
|
// the user's IdP.
|
|
self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) {
|
|
if (err) {
|
|
rp.doError(err);
|
|
return;
|
|
}
|
|
|
|
// The idpParams tell us where to go to provision and authenticate
|
|
// the identity.
|
|
self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) {
|
|
|
|
// Provision identity may have created a new provision flow
|
|
// for us. To make it easier to relate provision flows with
|
|
// RP callers, we cross index the two here.
|
|
rp.provId = aProvId;
|
|
self.IDP._provisionFlows[aProvId].rpId = aRPId;
|
|
|
|
// At this point, we already have a cert. If the user is also
|
|
// already authenticated with the IdP, then we can try again
|
|
// to generate an assertion and login.
|
|
if (err) {
|
|
// We are not authenticated. If we have already tried to
|
|
// authenticate and failed, then this is a "hard fail" and
|
|
// we give up. Otherwise we try to authenticate with the
|
|
// IdP.
|
|
|
|
if (self.IDP._provisionFlows[aProvId].didAuthentication) {
|
|
self.IDP._cleanUpProvisionFlow(aProvId);
|
|
self.RP._cleanUpProvisionFlow(aRPId, aProvId);
|
|
log("ERROR: selectIdentity: authentication hard fail");
|
|
rp.doError("Authentication fail.");
|
|
return;
|
|
}
|
|
// Try to authenticate with the IdP. Note that we do
|
|
// not clean up the provision flow here. We will continue
|
|
// to use it.
|
|
self.IDP._doAuthentication(aProvId, idpParams);
|
|
return;
|
|
}
|
|
|
|
// Provisioning flows end when a certificate has been registered.
|
|
// Thus IdentityProvider's registerCertificate() cleans up the
|
|
// current provisioning flow. We only do this here on error.
|
|
self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) {
|
|
if (err) {
|
|
rp.doError(err);
|
|
return;
|
|
}
|
|
self.RP._doLogin(rp, rpLoginOptions, assertion);
|
|
self.RP._cleanUpProvisionFlow(aRPId, aProvId);
|
|
return;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
// methods for chrome and add-ons
|
|
|
|
/**
|
|
* Discover the IdP for an identity
|
|
*
|
|
* @param aIdentity
|
|
* (string) the email we're logging in with
|
|
*
|
|
* @param aCallback
|
|
* (function) callback to invoke on completion
|
|
* with first-positional parameter the error.
|
|
*/
|
|
_discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
|
|
// XXX bug 767610 - validate email address call
|
|
// When that is available, we can remove this custom parser
|
|
var parsedEmail = this.parseEmail(aIdentity);
|
|
if (parsedEmail === null) {
|
|
aCallback("Could not parse email: " + aIdentity);
|
|
return;
|
|
}
|
|
log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
|
|
|
|
this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
|
|
// idpParams includes the pk, authorization url, and
|
|
// provisioning url.
|
|
|
|
// XXX bug 769861 follow any authority delegations
|
|
// if no well-known at any point in the delegation
|
|
// fall back to browserid.org as IdP
|
|
return aCallback(err, idpParams);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Fetch the well-known file from the domain.
|
|
*
|
|
* @param aDomain
|
|
*
|
|
* @param aScheme
|
|
* (string) (optional) Protocol to use. Default is https.
|
|
* This is necessary because we are unable to test
|
|
* https.
|
|
*
|
|
* @param aCallback
|
|
*
|
|
*/
|
|
_fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme = 'https') {
|
|
// XXX bug 769854 make tests https and remove aScheme option
|
|
let url = aScheme + '://' + aDomain + "/.well-known/browserid";
|
|
log("_fetchWellKnownFile:", url);
|
|
|
|
// this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
|
|
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance(Ci.nsIXMLHttpRequest);
|
|
|
|
// XXX bug 769865 gracefully handle being off-line
|
|
// XXX bug 769866 decide on how to handle redirects
|
|
req.open("GET", url, true);
|
|
req.responseType = "json";
|
|
req.mozBackgroundRequest = true;
|
|
req.onload = function _fetchWellKnownFile_onload() {
|
|
if (req.status < 200 || req.status >= 400) {
|
|
log("_fetchWellKnownFile", url, ": server returned status:", req.status);
|
|
return aCallback("Error");
|
|
}
|
|
try {
|
|
let idpParams = req.response;
|
|
|
|
// Verify that the IdP returned a valid configuration
|
|
if (!(idpParams.provisioning &&
|
|
idpParams.authentication &&
|
|
idpParams['public-key'])) {
|
|
let errStr = "Invalid well-known file from: " + aDomain;
|
|
log("_fetchWellKnownFile:", errStr);
|
|
return aCallback(errStr);
|
|
}
|
|
|
|
let callbackObj = {
|
|
domain: aDomain,
|
|
idpParams: idpParams,
|
|
};
|
|
log("_fetchWellKnownFile result: ", callbackObj);
|
|
// Yay. Valid IdP configuration for the domain.
|
|
return aCallback(null, callbackObj);
|
|
|
|
} catch (err) {
|
|
reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
|
|
return aCallback(err.toString());
|
|
}
|
|
};
|
|
req.onerror = function _fetchWellKnownFile_onerror() {
|
|
log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
|
|
log("ERROR: _fetchWellKnownFile:", err);
|
|
return aCallback("Error");
|
|
};
|
|
req.send(null);
|
|
},
|
|
|
|
};
|
|
|
|
this.IdentityService = new IDService();
|