1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/identity/RelyingParty.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,372 @@ 1.4 +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Ci = Components.interfaces; 1.14 +const Cc = Components.classes; 1.15 +const Cr = Components.results; 1.16 + 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/identity/IdentityStore.jsm"); 1.21 + 1.22 +this.EXPORTED_SYMBOLS = ["RelyingParty"]; 1.23 + 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "objectCopy", 1.25 + "resource://gre/modules/identity/IdentityUtils.jsm"); 1.26 + 1.27 +XPCOMUtils.defineLazyModuleGetter(this, 1.28 + "jwcrypto", 1.29 + "resource://gre/modules/identity/jwcrypto.jsm"); 1.30 + 1.31 +function log(...aMessageArgs) { 1.32 + Logger.log.apply(Logger, ["RP"].concat(aMessageArgs)); 1.33 +} 1.34 +function reportError(...aMessageArgs) { 1.35 + Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs)); 1.36 +} 1.37 + 1.38 +function IdentityRelyingParty() { 1.39 + // The store is a singleton shared among Identity, RelyingParty, and 1.40 + // IdentityProvider. The Identity module takes care of resetting 1.41 + // state in the _store on shutdown. 1.42 + this._store = IdentityStore; 1.43 + 1.44 + this.reset(); 1.45 +} 1.46 + 1.47 +IdentityRelyingParty.prototype = { 1.48 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), 1.49 + 1.50 + observe: function observe(aSubject, aTopic, aData) { 1.51 + switch (aTopic) { 1.52 + case "quit-application-granted": 1.53 + Services.obs.removeObserver(this, "quit-application-granted"); 1.54 + this.shutdown(); 1.55 + break; 1.56 + 1.57 + } 1.58 + }, 1.59 + 1.60 + reset: function RP_reset() { 1.61 + // Forget all documents that call in. (These are sometimes 1.62 + // referred to as callers.) 1.63 + this._rpFlows = {}; 1.64 + }, 1.65 + 1.66 + shutdown: function RP_shutdown() { 1.67 + this.reset(); 1.68 + Services.obs.removeObserver(this, "quit-application-granted"); 1.69 + }, 1.70 + 1.71 + /** 1.72 + * Register a listener for a given windowID as a result of a call to 1.73 + * navigator.id.watch(). 1.74 + * 1.75 + * @param aCaller 1.76 + * (Object) an object that represents the caller document, and 1.77 + * is expected to have properties: 1.78 + * - id (unique, e.g. uuid) 1.79 + * - loggedInUser (string or null) 1.80 + * - origin (string) 1.81 + * 1.82 + * and a bunch of callbacks 1.83 + * - doReady() 1.84 + * - doLogin() 1.85 + * - doLogout() 1.86 + * - doError() 1.87 + * - doCancel() 1.88 + * 1.89 + */ 1.90 + watch: function watch(aRpCaller) { 1.91 + this._rpFlows[aRpCaller.id] = aRpCaller; 1.92 + let origin = aRpCaller.origin; 1.93 + let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null }; 1.94 + 1.95 + log("watch: rpId:", aRpCaller.id, 1.96 + "origin:", origin, 1.97 + "loggedInUser:", aRpCaller.loggedInUser, 1.98 + "loggedIn:", state.isLoggedIn, 1.99 + "email:", state.email); 1.100 + 1.101 + // If the user is already logged in, then there are three cases 1.102 + // to deal with: 1.103 + // 1.104 + // 1. the email is valid and unchanged: 'ready' 1.105 + // 2. the email is null: 'login'; 'ready' 1.106 + // 3. the email has changed: 'login'; 'ready' 1.107 + if (state.isLoggedIn) { 1.108 + if (state.email && aRpCaller.loggedInUser === state.email) { 1.109 + this._notifyLoginStateChanged(aRpCaller.id, state.email); 1.110 + return aRpCaller.doReady(); 1.111 + 1.112 + } else if (aRpCaller.loggedInUser === null) { 1.113 + // Generate assertion for existing login 1.114 + let options = {loggedInUser: state.email, origin: origin}; 1.115 + return this._doLogin(aRpCaller, options); 1.116 + 1.117 + } else { 1.118 + // A loggedInUser different from state.email has been specified. 1.119 + // Change login identity. 1.120 + 1.121 + let options = {loggedInUser: state.email, origin: origin}; 1.122 + return this._doLogin(aRpCaller, options); 1.123 + } 1.124 + 1.125 + // If the user is not logged in, there are two cases: 1.126 + // 1.127 + // 1. a logged in email was provided: 'ready'; 'logout' 1.128 + // 2. not logged in, no email given: 'ready'; 1.129 + 1.130 + } else { 1.131 + if (aRpCaller.loggedInUser) { 1.132 + return this._doLogout(aRpCaller, {origin: origin}); 1.133 + 1.134 + } else { 1.135 + return aRpCaller.doReady(); 1.136 + } 1.137 + } 1.138 + }, 1.139 + 1.140 + /** 1.141 + * A utility for watch() to set state and notify the dom 1.142 + * on login 1.143 + * 1.144 + * Note that this calls _getAssertion 1.145 + */ 1.146 + _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) { 1.147 + log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin); 1.148 + 1.149 + let loginWithAssertion = function loginWithAssertion(assertion) { 1.150 + this._store.setLoginState(aOptions.origin, true, aOptions.loggedInUser); 1.151 + this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInUser); 1.152 + aRpCaller.doLogin(assertion); 1.153 + aRpCaller.doReady(); 1.154 + }.bind(this); 1.155 + 1.156 + if (aAssertion) { 1.157 + loginWithAssertion(aAssertion); 1.158 + } else { 1.159 + this._getAssertion(aOptions, function gotAssertion(err, assertion) { 1.160 + if (err) { 1.161 + reportError("_doLogin:", "Failed to get assertion on login attempt:", err); 1.162 + this._doLogout(aRpCaller); 1.163 + } else { 1.164 + loginWithAssertion(assertion); 1.165 + } 1.166 + }.bind(this)); 1.167 + } 1.168 + }, 1.169 + 1.170 + /** 1.171 + * A utility for watch() to set state and notify the dom 1.172 + * on logout. 1.173 + */ 1.174 + _doLogout: function _doLogout(aRpCaller, aOptions) { 1.175 + log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin); 1.176 + 1.177 + let state = this._store.getLoginState(aOptions.origin) || {}; 1.178 + 1.179 + state.isLoggedIn = false; 1.180 + this._notifyLoginStateChanged(aRpCaller.id, null); 1.181 + 1.182 + aRpCaller.doLogout(); 1.183 + aRpCaller.doReady(); 1.184 + }, 1.185 + 1.186 + /** 1.187 + * For use with login or logout, emit 'identity-login-state-changed' 1.188 + * 1.189 + * The notification will send the rp caller id in the properties, 1.190 + * and the email of the user in the message. 1.191 + * 1.192 + * @param aRpCallerId 1.193 + * (integer) The id of the RP caller 1.194 + * 1.195 + * @param aIdentity 1.196 + * (string) The email of the user whose login state has changed 1.197 + */ 1.198 + _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) { 1.199 + log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity); 1.200 + 1.201 + let options = {rpId: aRpCallerId}; 1.202 + Services.obs.notifyObservers({wrappedJSObject: options}, 1.203 + "identity-login-state-changed", 1.204 + aIdentity); 1.205 + }, 1.206 + 1.207 + /** 1.208 + * Initiate a login with user interaction as a result of a call to 1.209 + * navigator.id.request(). 1.210 + * 1.211 + * @param aRPId 1.212 + * (integer) the id of the doc object obtained in .watch() 1.213 + * 1.214 + * @param aOptions 1.215 + * (Object) options including privacyPolicy, termsOfService 1.216 + */ 1.217 + request: function request(aRPId, aOptions) { 1.218 + log("request: rpId:", aRPId); 1.219 + let rp = this._rpFlows[aRPId]; 1.220 + 1.221 + // Notify UX to display identity picker. 1.222 + // Pass the doc id to UX so it can pass it back to us later. 1.223 + let options = {rpId: aRPId, origin: rp.origin}; 1.224 + objectCopy(aOptions, options); 1.225 + 1.226 + // Append URLs after resolving 1.227 + let baseURI = Services.io.newURI(rp.origin, null, null); 1.228 + for (let optionName of ["privacyPolicy", "termsOfService"]) { 1.229 + if (aOptions[optionName]) { 1.230 + options[optionName] = baseURI.resolve(aOptions[optionName]); 1.231 + } 1.232 + } 1.233 + 1.234 + Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null); 1.235 + }, 1.236 + 1.237 + /** 1.238 + * Invoked when a user wishes to logout of a site (for instance, when clicking 1.239 + * on an in-content logout button). 1.240 + * 1.241 + * @param aRpCallerId 1.242 + * (integer) the id of the doc object obtained in .watch() 1.243 + * 1.244 + */ 1.245 + logout: function logout(aRpCallerId) { 1.246 + log("logout: RP caller id:", aRpCallerId); 1.247 + let rp = this._rpFlows[aRpCallerId]; 1.248 + if (rp && rp.origin) { 1.249 + let origin = rp.origin; 1.250 + log("logout: origin:", origin); 1.251 + this._doLogout(rp, {origin: origin}); 1.252 + } else { 1.253 + log("logout: no RP found with id:", aRpCallerId); 1.254 + } 1.255 + // We don't delete this._rpFlows[aRpCallerId], because 1.256 + // the user might log back in again. 1.257 + }, 1.258 + 1.259 + getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) { 1.260 + let identities = this.getIdentitiesForSite(aOrigin); 1.261 + let result = identities.lastUsed || null; 1.262 + log("getDefaultEmailForOrigin:", aOrigin, "->", result); 1.263 + return result; 1.264 + }, 1.265 + 1.266 + /** 1.267 + * Return the list of identities a user may want to use to login to aOrigin. 1.268 + */ 1.269 + getIdentitiesForSite: function getIdentitiesForSite(aOrigin) { 1.270 + let rv = { result: [] }; 1.271 + for (let id in this._store.getIdentities()) { 1.272 + rv.result.push(id); 1.273 + } 1.274 + let loginState = this._store.getLoginState(aOrigin); 1.275 + if (loginState && loginState.email) 1.276 + rv.lastUsed = loginState.email; 1.277 + return rv; 1.278 + }, 1.279 + 1.280 + /** 1.281 + * Obtain a BrowserID assertion with the specified characteristics. 1.282 + * 1.283 + * @param aCallback 1.284 + * (Function) Callback to be called with (err, assertion) where 'err' 1.285 + * can be an Error or NULL, and 'assertion' can be NULL or a valid 1.286 + * BrowserID assertion. If no callback is provided, an exception is 1.287 + * thrown. 1.288 + * 1.289 + * @param aOptions 1.290 + * (Object) An object that may contain the following properties: 1.291 + * 1.292 + * "audience" : The audience for which the assertion is to be 1.293 + * issued. If this property is not set an exception 1.294 + * will be thrown. 1.295 + * 1.296 + * Any properties not listed above will be ignored. 1.297 + */ 1.298 + _getAssertion: function _getAssertion(aOptions, aCallback) { 1.299 + let audience = aOptions.origin; 1.300 + let email = aOptions.loggedInUser || this.getDefaultEmailForOrigin(audience); 1.301 + log("_getAssertion: audience:", audience, "email:", email); 1.302 + if (!audience) { 1.303 + throw "audience required for _getAssertion"; 1.304 + } 1.305 + 1.306 + // We might not have any identity info for this email 1.307 + if (!this._store.fetchIdentity(email)) { 1.308 + this._store.addIdentity(email, null, null); 1.309 + } 1.310 + 1.311 + let cert = this._store.fetchIdentity(email)['cert']; 1.312 + if (cert) { 1.313 + this._generateAssertion(audience, email, function generatedAssertion(err, assertion) { 1.314 + if (err) { 1.315 + log("ERROR: _getAssertion:", err); 1.316 + } 1.317 + log("_getAssertion: generated assertion:", assertion); 1.318 + return aCallback(err, assertion); 1.319 + }); 1.320 + } 1.321 + }, 1.322 + 1.323 + /** 1.324 + * Generate an assertion, including provisioning via IdP if necessary, 1.325 + * but no user interaction, so if provisioning fails, aCallback is invoked 1.326 + * with an error. 1.327 + * 1.328 + * @param aAudience 1.329 + * (string) web origin 1.330 + * 1.331 + * @param aIdentity 1.332 + * (string) the email we're logging in with 1.333 + * 1.334 + * @param aCallback 1.335 + * (function) callback to invoke on completion 1.336 + * with first-positional parameter the error. 1.337 + */ 1.338 + _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) { 1.339 + log("_generateAssertion: audience:", aAudience, "identity:", aIdentity); 1.340 + 1.341 + let id = this._store.fetchIdentity(aIdentity); 1.342 + if (! (id && id.cert)) { 1.343 + let errStr = "Cannot generate an assertion without a certificate"; 1.344 + log("ERROR: _generateAssertion:", errStr); 1.345 + aCallback(errStr); 1.346 + return; 1.347 + } 1.348 + 1.349 + let kp = id.keyPair; 1.350 + 1.351 + if (!kp) { 1.352 + let errStr = "Cannot generate an assertion without a keypair"; 1.353 + log("ERROR: _generateAssertion:", errStr); 1.354 + aCallback(errStr); 1.355 + return; 1.356 + } 1.357 + 1.358 + jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback); 1.359 + }, 1.360 + 1.361 + /** 1.362 + * Clean up references to the provisioning flow for the specified RP. 1.363 + */ 1.364 + _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) { 1.365 + let rp = this._rpFlows[aRPId]; 1.366 + if (rp) { 1.367 + delete rp['provId']; 1.368 + } else { 1.369 + log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId); 1.370 + } 1.371 + }, 1.372 + 1.373 +}; 1.374 + 1.375 +this.RelyingParty = new IdentityRelyingParty();