2012-07-04 01:53:37 +04:00
|
|
|
/* -*- Mode: js2; js2-basic-offset: 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";
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.EXPORTED_SYMBOLS = ["RelyingParty"];
|
2012-07-04 01:53:37 +04:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this,
|
|
|
|
"jwcrypto",
|
|
|
|
"resource://gre/modules/identity/jwcrypto.jsm");
|
|
|
|
|
|
|
|
function log(...aMessageArgs) {
|
2012-07-11 10:54:27 +04:00
|
|
|
Logger.log.apply(Logger, ["RP"].concat(aMessageArgs));
|
2012-07-04 01:53:37 +04:00
|
|
|
}
|
|
|
|
function reportError(...aMessageArgs) {
|
2012-07-11 10:54:27 +04:00
|
|
|
Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
|
2012-07-04 01:53:37 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
function IdentityRelyingParty() {
|
|
|
|
// The store is a singleton shared among Identity, RelyingParty, and
|
|
|
|
// IdentityProvider. The Identity module takes care of resetting
|
|
|
|
// state in the _store on shutdown.
|
|
|
|
this._store = IdentityStore;
|
|
|
|
|
|
|
|
this.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
IdentityRelyingParty.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;
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
reset: function RP_reset() {
|
|
|
|
// Forget all documents that call in. (These are sometimes
|
|
|
|
// referred to as callers.)
|
|
|
|
this._rpFlows = {};
|
|
|
|
},
|
|
|
|
|
|
|
|
shutdown: function RP_shutdown() {
|
|
|
|
this.reset();
|
|
|
|
Services.obs.removeObserver(this, "quit-application-granted");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a listener for a given windowID as a result of a call to
|
|
|
|
* navigator.id.watch().
|
|
|
|
*
|
|
|
|
* @param aCaller
|
|
|
|
* (Object) an object that represents the caller document, and
|
|
|
|
* is expected to have properties:
|
|
|
|
* - id (unique, e.g. uuid)
|
|
|
|
* - loggedInEmail (string or null)
|
|
|
|
* - origin (string)
|
|
|
|
*
|
|
|
|
* and a bunch of callbacks
|
|
|
|
* - doReady()
|
|
|
|
* - doLogin()
|
|
|
|
* - doLogout()
|
|
|
|
* - doError()
|
|
|
|
* - doCancel()
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
watch: function watch(aRpCaller) {
|
|
|
|
this._rpFlows[aRpCaller.id] = aRpCaller;
|
|
|
|
let origin = aRpCaller.origin;
|
|
|
|
let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
|
|
|
|
|
|
|
|
log("watch: rpId:", aRpCaller.id,
|
|
|
|
"origin:", origin,
|
|
|
|
"loggedInEmail:", aRpCaller.loggedInEmail,
|
|
|
|
"loggedIn:", state.isLoggedIn,
|
|
|
|
"email:", state.email);
|
|
|
|
|
|
|
|
// If the user is already logged in, then there are three cases
|
|
|
|
// to deal with:
|
|
|
|
//
|
|
|
|
// 1. the email is valid and unchanged: 'ready'
|
|
|
|
// 2. the email is null: 'login'; 'ready'
|
|
|
|
// 3. the email has changed: 'login'; 'ready'
|
|
|
|
if (state.isLoggedIn) {
|
|
|
|
if (state.email && aRpCaller.loggedInEmail === state.email) {
|
|
|
|
this._notifyLoginStateChanged(aRpCaller.id, state.email);
|
|
|
|
return aRpCaller.doReady();
|
|
|
|
|
|
|
|
} else if (aRpCaller.loggedInEmail === null) {
|
|
|
|
// Generate assertion for existing login
|
|
|
|
let options = {loggedInEmail: state.email, origin: origin};
|
|
|
|
return this._doLogin(aRpCaller, options);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// A loggedInEmail different from state.email has been specified.
|
|
|
|
// Change login identity.
|
|
|
|
|
|
|
|
let options = {loggedInEmail: state.email, origin: origin};
|
|
|
|
return this._doLogin(aRpCaller, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the user is not logged in, there are two cases:
|
|
|
|
//
|
|
|
|
// 1. a logged in email was provided: 'ready'; 'logout'
|
|
|
|
// 2. not logged in, no email given: 'ready';
|
|
|
|
|
|
|
|
} else {
|
|
|
|
if (aRpCaller.loggedInEmail) {
|
|
|
|
return this._doLogout(aRpCaller, {origin: origin});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
return aRpCaller.doReady();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A utility for watch() to set state and notify the dom
|
|
|
|
* on login
|
|
|
|
*
|
|
|
|
* Note that this calls _getAssertion
|
|
|
|
*/
|
|
|
|
_doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
|
|
|
|
log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
|
|
|
|
|
|
|
|
let loginWithAssertion = function loginWithAssertion(assertion) {
|
|
|
|
this._store.setLoginState(aOptions.origin, true, aOptions.loggedInEmail);
|
|
|
|
this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInEmail);
|
|
|
|
aRpCaller.doLogin(assertion);
|
|
|
|
aRpCaller.doReady();
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
if (aAssertion) {
|
|
|
|
loginWithAssertion(aAssertion);
|
|
|
|
} else {
|
|
|
|
this._getAssertion(aOptions, function gotAssertion(err, assertion) {
|
|
|
|
if (err) {
|
|
|
|
reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
|
|
|
|
this._doLogout(aRpCaller);
|
|
|
|
} else {
|
|
|
|
loginWithAssertion(assertion);
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A utility for watch() to set state and notify the dom
|
|
|
|
* on logout.
|
|
|
|
*/
|
|
|
|
_doLogout: function _doLogout(aRpCaller, aOptions) {
|
|
|
|
log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
|
|
|
|
|
|
|
|
let state = this._store.getLoginState(aOptions.origin) || {};
|
|
|
|
|
|
|
|
state.isLoggedIn = false;
|
|
|
|
this._notifyLoginStateChanged(aRpCaller.id, null);
|
|
|
|
|
|
|
|
aRpCaller.doLogout();
|
|
|
|
aRpCaller.doReady();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For use with login or logout, emit 'identity-login-state-changed'
|
|
|
|
*
|
|
|
|
* The notification will send the rp caller id in the properties,
|
|
|
|
* and the email of the user in the message.
|
|
|
|
*
|
|
|
|
* @param aRpCallerId
|
|
|
|
* (integer) The id of the RP caller
|
|
|
|
*
|
|
|
|
* @param aIdentity
|
|
|
|
* (string) The email of the user whose login state has changed
|
|
|
|
*/
|
|
|
|
_notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
|
|
|
|
log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
|
|
|
|
|
|
|
|
let options = {rpId: aRpCallerId};
|
|
|
|
Services.obs.notifyObservers({wrappedJSObject: options},
|
|
|
|
"identity-login-state-changed",
|
|
|
|
aIdentity);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initiate a login with user interaction as a result of a call to
|
|
|
|
* navigator.id.request().
|
|
|
|
*
|
|
|
|
* @param aRPId
|
|
|
|
* (integer) the id of the doc object obtained in .watch()
|
|
|
|
*
|
|
|
|
* @param aOptions
|
|
|
|
* (Object) options including privacyPolicy, termsOfService
|
|
|
|
*/
|
|
|
|
request: function request(aRPId, aOptions) {
|
|
|
|
log("request: rpId:", aRPId);
|
2012-06-13 05:16:00 +04:00
|
|
|
let rp = this._rpFlows[aRPId];
|
2012-07-04 01:53:37 +04:00
|
|
|
|
|
|
|
// Notify UX to display identity picker.
|
|
|
|
// Pass the doc id to UX so it can pass it back to us later.
|
2012-06-13 05:16:00 +04:00
|
|
|
let options = {rpId: aRPId, origin: rp.origin};
|
2012-07-04 01:53:37 +04:00
|
|
|
|
|
|
|
// Append URLs after resolving
|
|
|
|
let baseURI = Services.io.newURI(rp.origin, null, null);
|
|
|
|
for (let optionName of ["privacyPolicy", "termsOfService"]) {
|
|
|
|
if (aOptions[optionName]) {
|
|
|
|
options[optionName] = baseURI.resolve(aOptions[optionName]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoked when a user wishes to logout of a site (for instance, when clicking
|
|
|
|
* on an in-content logout button).
|
|
|
|
*
|
|
|
|
* @param aRpCallerId
|
|
|
|
* (integer) the id of the doc object obtained in .watch()
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
logout: function logout(aRpCallerId) {
|
|
|
|
log("logout: RP caller id:", aRpCallerId);
|
|
|
|
let rp = this._rpFlows[aRpCallerId];
|
|
|
|
if (rp && rp.origin) {
|
|
|
|
let origin = rp.origin;
|
|
|
|
log("logout: origin:", origin);
|
|
|
|
this._doLogout(rp, {origin: origin});
|
|
|
|
} else {
|
|
|
|
log("logout: no RP found with id:", aRpCallerId);
|
|
|
|
}
|
|
|
|
// We don't delete this._rpFlows[aRpCallerId], because
|
|
|
|
// the user might log back in again.
|
|
|
|
},
|
|
|
|
|
|
|
|
getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
|
|
|
|
let identities = this.getIdentitiesForSite(aOrigin);
|
|
|
|
let result = identities.lastUsed || null;
|
|
|
|
log("getDefaultEmailForOrigin:", aOrigin, "->", result);
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the list of identities a user may want to use to login to aOrigin.
|
|
|
|
*/
|
|
|
|
getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
|
|
|
|
let rv = { result: [] };
|
|
|
|
for (let id in this._store.getIdentities()) {
|
|
|
|
rv.result.push(id);
|
|
|
|
}
|
|
|
|
let loginState = this._store.getLoginState(aOrigin);
|
|
|
|
if (loginState && loginState.email)
|
|
|
|
rv.lastUsed = loginState.email;
|
|
|
|
return rv;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Obtain a BrowserID assertion with the specified characteristics.
|
|
|
|
*
|
|
|
|
* @param aCallback
|
|
|
|
* (Function) Callback to be called with (err, assertion) where 'err'
|
|
|
|
* can be an Error or NULL, and 'assertion' can be NULL or a valid
|
|
|
|
* BrowserID assertion. If no callback is provided, an exception is
|
|
|
|
* thrown.
|
|
|
|
*
|
|
|
|
* @param aOptions
|
|
|
|
* (Object) An object that may contain the following properties:
|
|
|
|
*
|
|
|
|
* "audience" : The audience for which the assertion is to be
|
|
|
|
* issued. If this property is not set an exception
|
|
|
|
* will be thrown.
|
|
|
|
*
|
|
|
|
* Any properties not listed above will be ignored.
|
|
|
|
*/
|
|
|
|
_getAssertion: function _getAssertion(aOptions, aCallback) {
|
|
|
|
let audience = aOptions.origin;
|
|
|
|
let email = aOptions.loggedInEmail || this.getDefaultEmailForOrigin(audience);
|
|
|
|
log("_getAssertion: audience:", audience, "email:", email);
|
|
|
|
if (!audience) {
|
|
|
|
throw "audience required for _getAssertion";
|
|
|
|
}
|
|
|
|
|
|
|
|
// We might not have any identity info for this email
|
|
|
|
if (!this._store.fetchIdentity(email)) {
|
|
|
|
this._store.addIdentity(email, null, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
let cert = this._store.fetchIdentity(email)['cert'];
|
|
|
|
if (cert) {
|
|
|
|
this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
|
|
|
|
if (err) {
|
|
|
|
log("ERROR: _getAssertion:", err);
|
|
|
|
}
|
|
|
|
log("_getAssertion: generated assertion:", assertion);
|
|
|
|
return aCallback(err, assertion);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate an assertion, including provisioning via IdP if necessary,
|
|
|
|
* but no user interaction, so if provisioning fails, aCallback is invoked
|
|
|
|
* with an error.
|
|
|
|
*
|
|
|
|
* @param aAudience
|
|
|
|
* (string) web origin
|
|
|
|
*
|
|
|
|
* @param aIdentity
|
|
|
|
* (string) the email we're logging in with
|
|
|
|
*
|
|
|
|
* @param aCallback
|
|
|
|
* (function) callback to invoke on completion
|
|
|
|
* with first-positional parameter the error.
|
|
|
|
*/
|
|
|
|
_generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
|
|
|
|
log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
|
|
|
|
|
|
|
|
let id = this._store.fetchIdentity(aIdentity);
|
|
|
|
if (! (id && id.cert)) {
|
|
|
|
let errStr = "Cannot generate an assertion without a certificate";
|
|
|
|
log("ERROR: _generateAssertion:", errStr);
|
|
|
|
aCallback(errStr);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let kp = id.keyPair;
|
|
|
|
|
|
|
|
if (!kp) {
|
|
|
|
let errStr = "Cannot generate an assertion without a keypair";
|
|
|
|
log("ERROR: _generateAssertion:", errStr);
|
|
|
|
aCallback(errStr);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up references to the provisioning flow for the specified RP.
|
|
|
|
*/
|
|
|
|
_cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
|
|
|
|
let rp = this._rpFlows[aRPId];
|
|
|
|
if (rp) {
|
|
|
|
delete rp['provId'];
|
|
|
|
} else {
|
|
|
|
log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.RelyingParty = new IdentityRelyingParty();
|