toolkit/identity/RelyingParty.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Ci = Components.interfaces;
michael@0 11 const Cc = Components.classes;
michael@0 12 const Cr = Components.results;
michael@0 13
michael@0 14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15 Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
michael@0 17 Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
michael@0 18
michael@0 19 this.EXPORTED_SYMBOLS = ["RelyingParty"];
michael@0 20
michael@0 21 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
michael@0 22 "resource://gre/modules/identity/IdentityUtils.jsm");
michael@0 23
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this,
michael@0 25 "jwcrypto",
michael@0 26 "resource://gre/modules/identity/jwcrypto.jsm");
michael@0 27
michael@0 28 function log(...aMessageArgs) {
michael@0 29 Logger.log.apply(Logger, ["RP"].concat(aMessageArgs));
michael@0 30 }
michael@0 31 function reportError(...aMessageArgs) {
michael@0 32 Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
michael@0 33 }
michael@0 34
michael@0 35 function IdentityRelyingParty() {
michael@0 36 // The store is a singleton shared among Identity, RelyingParty, and
michael@0 37 // IdentityProvider. The Identity module takes care of resetting
michael@0 38 // state in the _store on shutdown.
michael@0 39 this._store = IdentityStore;
michael@0 40
michael@0 41 this.reset();
michael@0 42 }
michael@0 43
michael@0 44 IdentityRelyingParty.prototype = {
michael@0 45 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
michael@0 46
michael@0 47 observe: function observe(aSubject, aTopic, aData) {
michael@0 48 switch (aTopic) {
michael@0 49 case "quit-application-granted":
michael@0 50 Services.obs.removeObserver(this, "quit-application-granted");
michael@0 51 this.shutdown();
michael@0 52 break;
michael@0 53
michael@0 54 }
michael@0 55 },
michael@0 56
michael@0 57 reset: function RP_reset() {
michael@0 58 // Forget all documents that call in. (These are sometimes
michael@0 59 // referred to as callers.)
michael@0 60 this._rpFlows = {};
michael@0 61 },
michael@0 62
michael@0 63 shutdown: function RP_shutdown() {
michael@0 64 this.reset();
michael@0 65 Services.obs.removeObserver(this, "quit-application-granted");
michael@0 66 },
michael@0 67
michael@0 68 /**
michael@0 69 * Register a listener for a given windowID as a result of a call to
michael@0 70 * navigator.id.watch().
michael@0 71 *
michael@0 72 * @param aCaller
michael@0 73 * (Object) an object that represents the caller document, and
michael@0 74 * is expected to have properties:
michael@0 75 * - id (unique, e.g. uuid)
michael@0 76 * - loggedInUser (string or null)
michael@0 77 * - origin (string)
michael@0 78 *
michael@0 79 * and a bunch of callbacks
michael@0 80 * - doReady()
michael@0 81 * - doLogin()
michael@0 82 * - doLogout()
michael@0 83 * - doError()
michael@0 84 * - doCancel()
michael@0 85 *
michael@0 86 */
michael@0 87 watch: function watch(aRpCaller) {
michael@0 88 this._rpFlows[aRpCaller.id] = aRpCaller;
michael@0 89 let origin = aRpCaller.origin;
michael@0 90 let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
michael@0 91
michael@0 92 log("watch: rpId:", aRpCaller.id,
michael@0 93 "origin:", origin,
michael@0 94 "loggedInUser:", aRpCaller.loggedInUser,
michael@0 95 "loggedIn:", state.isLoggedIn,
michael@0 96 "email:", state.email);
michael@0 97
michael@0 98 // If the user is already logged in, then there are three cases
michael@0 99 // to deal with:
michael@0 100 //
michael@0 101 // 1. the email is valid and unchanged: 'ready'
michael@0 102 // 2. the email is null: 'login'; 'ready'
michael@0 103 // 3. the email has changed: 'login'; 'ready'
michael@0 104 if (state.isLoggedIn) {
michael@0 105 if (state.email && aRpCaller.loggedInUser === state.email) {
michael@0 106 this._notifyLoginStateChanged(aRpCaller.id, state.email);
michael@0 107 return aRpCaller.doReady();
michael@0 108
michael@0 109 } else if (aRpCaller.loggedInUser === null) {
michael@0 110 // Generate assertion for existing login
michael@0 111 let options = {loggedInUser: state.email, origin: origin};
michael@0 112 return this._doLogin(aRpCaller, options);
michael@0 113
michael@0 114 } else {
michael@0 115 // A loggedInUser different from state.email has been specified.
michael@0 116 // Change login identity.
michael@0 117
michael@0 118 let options = {loggedInUser: state.email, origin: origin};
michael@0 119 return this._doLogin(aRpCaller, options);
michael@0 120 }
michael@0 121
michael@0 122 // If the user is not logged in, there are two cases:
michael@0 123 //
michael@0 124 // 1. a logged in email was provided: 'ready'; 'logout'
michael@0 125 // 2. not logged in, no email given: 'ready';
michael@0 126
michael@0 127 } else {
michael@0 128 if (aRpCaller.loggedInUser) {
michael@0 129 return this._doLogout(aRpCaller, {origin: origin});
michael@0 130
michael@0 131 } else {
michael@0 132 return aRpCaller.doReady();
michael@0 133 }
michael@0 134 }
michael@0 135 },
michael@0 136
michael@0 137 /**
michael@0 138 * A utility for watch() to set state and notify the dom
michael@0 139 * on login
michael@0 140 *
michael@0 141 * Note that this calls _getAssertion
michael@0 142 */
michael@0 143 _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
michael@0 144 log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
michael@0 145
michael@0 146 let loginWithAssertion = function loginWithAssertion(assertion) {
michael@0 147 this._store.setLoginState(aOptions.origin, true, aOptions.loggedInUser);
michael@0 148 this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInUser);
michael@0 149 aRpCaller.doLogin(assertion);
michael@0 150 aRpCaller.doReady();
michael@0 151 }.bind(this);
michael@0 152
michael@0 153 if (aAssertion) {
michael@0 154 loginWithAssertion(aAssertion);
michael@0 155 } else {
michael@0 156 this._getAssertion(aOptions, function gotAssertion(err, assertion) {
michael@0 157 if (err) {
michael@0 158 reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
michael@0 159 this._doLogout(aRpCaller);
michael@0 160 } else {
michael@0 161 loginWithAssertion(assertion);
michael@0 162 }
michael@0 163 }.bind(this));
michael@0 164 }
michael@0 165 },
michael@0 166
michael@0 167 /**
michael@0 168 * A utility for watch() to set state and notify the dom
michael@0 169 * on logout.
michael@0 170 */
michael@0 171 _doLogout: function _doLogout(aRpCaller, aOptions) {
michael@0 172 log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
michael@0 173
michael@0 174 let state = this._store.getLoginState(aOptions.origin) || {};
michael@0 175
michael@0 176 state.isLoggedIn = false;
michael@0 177 this._notifyLoginStateChanged(aRpCaller.id, null);
michael@0 178
michael@0 179 aRpCaller.doLogout();
michael@0 180 aRpCaller.doReady();
michael@0 181 },
michael@0 182
michael@0 183 /**
michael@0 184 * For use with login or logout, emit 'identity-login-state-changed'
michael@0 185 *
michael@0 186 * The notification will send the rp caller id in the properties,
michael@0 187 * and the email of the user in the message.
michael@0 188 *
michael@0 189 * @param aRpCallerId
michael@0 190 * (integer) The id of the RP caller
michael@0 191 *
michael@0 192 * @param aIdentity
michael@0 193 * (string) The email of the user whose login state has changed
michael@0 194 */
michael@0 195 _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
michael@0 196 log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
michael@0 197
michael@0 198 let options = {rpId: aRpCallerId};
michael@0 199 Services.obs.notifyObservers({wrappedJSObject: options},
michael@0 200 "identity-login-state-changed",
michael@0 201 aIdentity);
michael@0 202 },
michael@0 203
michael@0 204 /**
michael@0 205 * Initiate a login with user interaction as a result of a call to
michael@0 206 * navigator.id.request().
michael@0 207 *
michael@0 208 * @param aRPId
michael@0 209 * (integer) the id of the doc object obtained in .watch()
michael@0 210 *
michael@0 211 * @param aOptions
michael@0 212 * (Object) options including privacyPolicy, termsOfService
michael@0 213 */
michael@0 214 request: function request(aRPId, aOptions) {
michael@0 215 log("request: rpId:", aRPId);
michael@0 216 let rp = this._rpFlows[aRPId];
michael@0 217
michael@0 218 // Notify UX to display identity picker.
michael@0 219 // Pass the doc id to UX so it can pass it back to us later.
michael@0 220 let options = {rpId: aRPId, origin: rp.origin};
michael@0 221 objectCopy(aOptions, options);
michael@0 222
michael@0 223 // Append URLs after resolving
michael@0 224 let baseURI = Services.io.newURI(rp.origin, null, null);
michael@0 225 for (let optionName of ["privacyPolicy", "termsOfService"]) {
michael@0 226 if (aOptions[optionName]) {
michael@0 227 options[optionName] = baseURI.resolve(aOptions[optionName]);
michael@0 228 }
michael@0 229 }
michael@0 230
michael@0 231 Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
michael@0 232 },
michael@0 233
michael@0 234 /**
michael@0 235 * Invoked when a user wishes to logout of a site (for instance, when clicking
michael@0 236 * on an in-content logout button).
michael@0 237 *
michael@0 238 * @param aRpCallerId
michael@0 239 * (integer) the id of the doc object obtained in .watch()
michael@0 240 *
michael@0 241 */
michael@0 242 logout: function logout(aRpCallerId) {
michael@0 243 log("logout: RP caller id:", aRpCallerId);
michael@0 244 let rp = this._rpFlows[aRpCallerId];
michael@0 245 if (rp && rp.origin) {
michael@0 246 let origin = rp.origin;
michael@0 247 log("logout: origin:", origin);
michael@0 248 this._doLogout(rp, {origin: origin});
michael@0 249 } else {
michael@0 250 log("logout: no RP found with id:", aRpCallerId);
michael@0 251 }
michael@0 252 // We don't delete this._rpFlows[aRpCallerId], because
michael@0 253 // the user might log back in again.
michael@0 254 },
michael@0 255
michael@0 256 getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
michael@0 257 let identities = this.getIdentitiesForSite(aOrigin);
michael@0 258 let result = identities.lastUsed || null;
michael@0 259 log("getDefaultEmailForOrigin:", aOrigin, "->", result);
michael@0 260 return result;
michael@0 261 },
michael@0 262
michael@0 263 /**
michael@0 264 * Return the list of identities a user may want to use to login to aOrigin.
michael@0 265 */
michael@0 266 getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
michael@0 267 let rv = { result: [] };
michael@0 268 for (let id in this._store.getIdentities()) {
michael@0 269 rv.result.push(id);
michael@0 270 }
michael@0 271 let loginState = this._store.getLoginState(aOrigin);
michael@0 272 if (loginState && loginState.email)
michael@0 273 rv.lastUsed = loginState.email;
michael@0 274 return rv;
michael@0 275 },
michael@0 276
michael@0 277 /**
michael@0 278 * Obtain a BrowserID assertion with the specified characteristics.
michael@0 279 *
michael@0 280 * @param aCallback
michael@0 281 * (Function) Callback to be called with (err, assertion) where 'err'
michael@0 282 * can be an Error or NULL, and 'assertion' can be NULL or a valid
michael@0 283 * BrowserID assertion. If no callback is provided, an exception is
michael@0 284 * thrown.
michael@0 285 *
michael@0 286 * @param aOptions
michael@0 287 * (Object) An object that may contain the following properties:
michael@0 288 *
michael@0 289 * "audience" : The audience for which the assertion is to be
michael@0 290 * issued. If this property is not set an exception
michael@0 291 * will be thrown.
michael@0 292 *
michael@0 293 * Any properties not listed above will be ignored.
michael@0 294 */
michael@0 295 _getAssertion: function _getAssertion(aOptions, aCallback) {
michael@0 296 let audience = aOptions.origin;
michael@0 297 let email = aOptions.loggedInUser || this.getDefaultEmailForOrigin(audience);
michael@0 298 log("_getAssertion: audience:", audience, "email:", email);
michael@0 299 if (!audience) {
michael@0 300 throw "audience required for _getAssertion";
michael@0 301 }
michael@0 302
michael@0 303 // We might not have any identity info for this email
michael@0 304 if (!this._store.fetchIdentity(email)) {
michael@0 305 this._store.addIdentity(email, null, null);
michael@0 306 }
michael@0 307
michael@0 308 let cert = this._store.fetchIdentity(email)['cert'];
michael@0 309 if (cert) {
michael@0 310 this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
michael@0 311 if (err) {
michael@0 312 log("ERROR: _getAssertion:", err);
michael@0 313 }
michael@0 314 log("_getAssertion: generated assertion:", assertion);
michael@0 315 return aCallback(err, assertion);
michael@0 316 });
michael@0 317 }
michael@0 318 },
michael@0 319
michael@0 320 /**
michael@0 321 * Generate an assertion, including provisioning via IdP if necessary,
michael@0 322 * but no user interaction, so if provisioning fails, aCallback is invoked
michael@0 323 * with an error.
michael@0 324 *
michael@0 325 * @param aAudience
michael@0 326 * (string) web origin
michael@0 327 *
michael@0 328 * @param aIdentity
michael@0 329 * (string) the email we're logging in with
michael@0 330 *
michael@0 331 * @param aCallback
michael@0 332 * (function) callback to invoke on completion
michael@0 333 * with first-positional parameter the error.
michael@0 334 */
michael@0 335 _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
michael@0 336 log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
michael@0 337
michael@0 338 let id = this._store.fetchIdentity(aIdentity);
michael@0 339 if (! (id && id.cert)) {
michael@0 340 let errStr = "Cannot generate an assertion without a certificate";
michael@0 341 log("ERROR: _generateAssertion:", errStr);
michael@0 342 aCallback(errStr);
michael@0 343 return;
michael@0 344 }
michael@0 345
michael@0 346 let kp = id.keyPair;
michael@0 347
michael@0 348 if (!kp) {
michael@0 349 let errStr = "Cannot generate an assertion without a keypair";
michael@0 350 log("ERROR: _generateAssertion:", errStr);
michael@0 351 aCallback(errStr);
michael@0 352 return;
michael@0 353 }
michael@0 354
michael@0 355 jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
michael@0 356 },
michael@0 357
michael@0 358 /**
michael@0 359 * Clean up references to the provisioning flow for the specified RP.
michael@0 360 */
michael@0 361 _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
michael@0 362 let rp = this._rpFlows[aRPId];
michael@0 363 if (rp) {
michael@0 364 delete rp['provId'];
michael@0 365 } else {
michael@0 366 log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
michael@0 367 }
michael@0 368 },
michael@0 369
michael@0 370 };
michael@0 371
michael@0 372 this.RelyingParty = new IdentityRelyingParty();

mercurial