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