OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * Authenticator class wraps the communications between Gaia and its host. | |
7 */ | |
8 function Authenticator() { | |
9 } | |
10 | |
11 /** | |
12 * Gaia auth extension url origin. | |
13 * @type {string} | |
14 */ | |
15 Authenticator.THIS_EXTENSION_ORIGIN = | |
16 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik'; | |
17 | |
18 /** | |
19 * The lowest version of the credentials passing API supported. | |
20 * @type {number} | |
21 */ | |
22 Authenticator.MIN_API_VERSION_VERSION = 1; | |
23 | |
24 /** | |
25 * The highest version of the credentials passing API supported. | |
26 * @type {number} | |
27 */ | |
28 Authenticator.MAX_API_VERSION_VERSION = 1; | |
29 | |
30 /** | |
31 * The key types supported by the credentials passing API. | |
32 * @type {Array} Array of strings. | |
33 */ | |
34 Authenticator.API_KEY_TYPES = [ | |
35 'KEY_TYPE_PASSWORD_PLAIN', | |
36 ]; | |
37 | |
38 /** | |
39 * Allowed origins of the hosting page. | |
40 * @type {Array<string>} | |
41 */ | |
42 Authenticator.ALLOWED_PARENT_ORIGINS = [ | |
43 'chrome://oobe', | |
44 'chrome://chrome-signin' | |
45 ]; | |
46 | |
47 /** | |
48 * Singleton getter of Authenticator. | |
49 * @return {Object} The singleton instance of Authenticator. | |
50 */ | |
51 Authenticator.getInstance = function() { | |
52 if (!Authenticator.instance_) { | |
53 Authenticator.instance_ = new Authenticator(); | |
54 } | |
55 return Authenticator.instance_; | |
56 }; | |
57 | |
58 Authenticator.prototype = { | |
59 email_: null, | |
60 gaiaId_: null, | |
61 | |
62 // Depending on the key type chosen, this will contain the plain text password | |
63 // or a credential derived from it along with the information required to | |
64 // repeat the derivation, such as a salt. The information will be encoded so | |
65 // that it contains printable ASCII characters only. The exact encoding is TBD | |
66 // when support for key types other than plain text password is added. | |
67 passwordBytes_: null, | |
68 | |
69 needPassword_: false, | |
70 chooseWhatToSync_: false, | |
71 skipForNow_: false, | |
72 sessionIndex_: null, | |
73 attemptToken_: null, | |
74 | |
75 // Input params from extension initialization URL. | |
76 inputLang_: undefined, | |
77 intputEmail_: undefined, | |
78 | |
79 isSAMLFlow_: false, | |
80 gaiaLoaded_: false, | |
81 supportChannel_: null, | |
82 | |
83 useEafe_: false, | |
84 clientId_: '', | |
85 | |
86 GAIA_URL: 'https://accounts.google.com/', | |
87 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', | |
88 SERVICE_ID: 'chromeoslogin', | |
89 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', | |
90 CONSTRAINED_FLOW_SOURCE: 'chrome', | |
91 | |
92 initialize: function() { | |
93 var handleInitializeMessage = function(e) { | |
94 if (Authenticator.ALLOWED_PARENT_ORIGINS.indexOf(e.origin) == -1) { | |
95 console.error('Unexpected parent message, origin=' + e.origin); | |
96 return; | |
97 } | |
98 window.removeEventListener('message', handleInitializeMessage); | |
99 | |
100 var params = e.data; | |
101 params.parentPage = e.origin; | |
102 this.initializeFromParent_(params); | |
103 this.onPageLoad_(); | |
104 }.bind(this); | |
105 | |
106 document.addEventListener('DOMContentLoaded', function() { | |
107 window.addEventListener('message', handleInitializeMessage); | |
108 window.parent.postMessage({'method': 'loginUIDOMContentLoaded'}, '*'); | |
109 }); | |
110 }, | |
111 | |
112 initializeFromParent_: function(params) { | |
113 this.parentPage_ = params.parentPage; | |
114 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL; | |
115 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH; | |
116 this.inputLang_ = params.hl; | |
117 this.inputEmail_ = params.email; | |
118 this.service_ = params.service || this.SERVICE_ID; | |
119 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; | |
120 this.desktopMode_ = params.desktopMode == '1'; | |
121 this.isConstrainedWindow_ = params.constrained == '1'; | |
122 this.useEafe_ = params.useEafe || false; | |
123 this.clientId_ = params.clientId || ''; | |
124 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); | |
125 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); | |
126 this.needPassword_ = params.needPassword == '1'; | |
127 | |
128 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved | |
129 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this | |
130 // message so we have to rely on 'load' event. | |
131 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed. | |
132 this.assumeLoadedOnLoadEvent_ = | |
133 !this.gaiaPath_.startsWith('ServiceLogin') || | |
134 this.service_ !== 'chromeoslogin' || | |
135 this.useEafe_; | |
136 }, | |
137 | |
138 isGaiaMessage_: function(msg) { | |
139 // Not quite right, but good enough. | |
140 return this.gaiaUrl_.startsWith(msg.origin) || | |
141 this.GAIA_URL.startsWith(msg.origin); | |
142 }, | |
143 | |
144 isParentMessage_: function(msg) { | |
145 return msg.origin == this.parentPage_; | |
146 }, | |
147 | |
148 constructInitialFrameUrl_: function() { | |
149 var url = this.gaiaUrl_ + this.gaiaPath_; | |
150 | |
151 url = appendParam(url, 'service', this.service_); | |
152 // Easy bootstrap use auth_code message as success signal instead of | |
153 // continue URL. | |
154 if (!this.useEafe_) | |
155 url = appendParam(url, 'continue', this.continueUrl_); | |
156 if (this.inputLang_) | |
157 url = appendParam(url, 'hl', this.inputLang_); | |
158 if (this.inputEmail_) | |
159 url = appendParam(url, 'Email', this.inputEmail_); | |
160 if (this.isConstrainedWindow_) | |
161 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE); | |
162 return url; | |
163 }, | |
164 | |
165 onPageLoad_: function() { | |
166 window.addEventListener('message', this.onMessage.bind(this), false); | |
167 this.initSupportChannel_(); | |
168 | |
169 if (this.assumeLoadedOnLoadEvent_) { | |
170 var gaiaFrame = $('gaia-frame'); | |
171 var handler = function() { | |
172 gaiaFrame.removeEventListener('load', handler); | |
173 if (!this.gaiaLoaded_) { | |
174 this.gaiaLoaded_ = true; | |
175 this.maybeInitialized_(); | |
176 | |
177 if (this.useEafe_ && this.clientId_) { | |
178 // Sends initial handshake message to EAFE. Note this fails with | |
179 // SSO redirect because |gaiaFrame| sits on a different origin. | |
180 gaiaFrame.contentWindow.postMessage({ | |
181 clientId: this.clientId_ | |
182 }, this.gaiaUrl_); | |
183 } | |
184 } | |
185 }.bind(this); | |
186 gaiaFrame.addEventListener('load', handler); | |
187 } | |
188 }, | |
189 | |
190 initSupportChannel_: function() { | |
191 var supportChannel = new Channel(); | |
192 supportChannel.connect('authMain'); | |
193 | |
194 supportChannel.registerMessage('channelConnected', function() { | |
195 // Load the gaia frame after the background page indicates that it is | |
196 // ready, so that the webRequest handlers are all setup first. | |
197 var gaiaFrame = $('gaia-frame'); | |
198 gaiaFrame.src = this.initialFrameUrl_; | |
199 | |
200 if (this.supportChannel_) { | |
201 console.error('Support channel is already initialized.'); | |
202 return; | |
203 } | |
204 this.supportChannel_ = supportChannel; | |
205 | |
206 if (this.desktopMode_) { | |
207 this.supportChannel_.send({ | |
208 name: 'initDesktopFlow', | |
209 gaiaUrl: this.gaiaUrl_, | |
210 continueUrl: stripParams(this.continueUrl_), | |
211 isConstrainedWindow: this.isConstrainedWindow_, | |
212 initialFrameUrlWithoutParams: this.initialFrameUrlWithoutParams_ | |
213 }); | |
214 | |
215 this.supportChannel_.registerMessage( | |
216 'switchToFullTab', this.switchToFullTab_.bind(this)); | |
217 } | |
218 this.supportChannel_.registerMessage( | |
219 'completeLogin', this.onCompleteLogin_.bind(this)); | |
220 this.initSAML_(); | |
221 this.supportChannel_.send({name: 'resetAuth'}); | |
222 this.maybeInitialized_(); | |
223 }.bind(this)); | |
224 | |
225 window.setTimeout(function() { | |
226 if (!this.supportChannel_) { | |
227 // Give up previous channel and bind its 'channelConnected' to a no-op. | |
228 supportChannel.registerMessage('channelConnected', function() {}); | |
229 | |
230 // Re-initialize the channel if it is not connected properly, e.g. | |
231 // connect may be called before background script started running. | |
232 this.initSupportChannel_(); | |
233 } | |
234 }.bind(this), 200); | |
235 }, | |
236 | |
237 /** | |
238 * Called when one of the initialization stages has finished. If all the | |
239 * needed parts are initialized, notifies parent about successfull | |
240 * initialization. | |
241 */ | |
242 maybeInitialized_: function() { | |
243 if (!this.gaiaLoaded_ || !this.supportChannel_) | |
244 return; | |
245 var msg = { | |
246 'method': 'loginUILoaded' | |
247 }; | |
248 window.parent.postMessage(msg, this.parentPage_); | |
249 }, | |
250 | |
251 /** | |
252 * Invoked when the background script sends a message to indicate that the | |
253 * current content does not fit in a constrained window. | |
254 * @param {Object=} msg Extra info to send. | |
255 */ | |
256 switchToFullTab_: function(msg) { | |
257 var parentMsg = { | |
258 'method': 'switchToFullTab', | |
259 'url': msg.url | |
260 }; | |
261 window.parent.postMessage(parentMsg, this.parentPage_); | |
262 }, | |
263 | |
264 /** | |
265 * Invoked when the signin flow is complete. | |
266 * @param {Object=} opt_extraMsg Optional extra info to send. | |
267 */ | |
268 completeLogin_: function(opt_extraMsg) { | |
269 var msg = { | |
270 'method': 'completeLogin', | |
271 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_, | |
272 'password': this.passwordBytes_ || | |
273 (opt_extraMsg && opt_extraMsg.password), | |
274 'usingSAML': this.isSAMLFlow_, | |
275 'chooseWhatToSync': this.chooseWhatToSync_ || false, | |
276 'skipForNow': (opt_extraMsg && opt_extraMsg.skipForNow) || | |
277 this.skipForNow_, | |
278 'sessionIndex': (opt_extraMsg && opt_extraMsg.sessionIndex) || | |
279 this.sessionIndex_, | |
280 'gaiaId': (opt_extraMsg && opt_extraMsg.gaiaId) || this.gaiaId_ | |
281 }; | |
282 window.parent.postMessage(msg, this.parentPage_); | |
283 this.supportChannel_.send({name: 'resetAuth'}); | |
284 }, | |
285 | |
286 /** | |
287 * Invoked when support channel is connected. | |
288 */ | |
289 initSAML_: function() { | |
290 this.isSAMLFlow_ = false; | |
291 | |
292 this.supportChannel_.registerMessage( | |
293 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); | |
294 this.supportChannel_.registerMessage( | |
295 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); | |
296 this.supportChannel_.registerMessage( | |
297 'apiCall', this.onAPICall_.bind(this)); | |
298 this.supportChannel_.send({ | |
299 name: 'setGaiaUrl', | |
300 gaiaUrl: this.gaiaUrl_ | |
301 }); | |
302 if (!this.desktopMode_ && this.gaiaUrl_.startsWith('https://')) { | |
303 // Abort the login flow when content served over an unencrypted connection | |
304 // is detected on Chrome OS. This does not apply to tests that explicitly | |
305 // set a non-https GAIA URL and want to perform all authentication over | |
306 // http. | |
307 this.supportChannel_.send({ | |
308 name: 'setBlockInsecureContent', | |
309 blockInsecureContent: true | |
310 }); | |
311 } | |
312 }, | |
313 | |
314 /** | |
315 * Invoked when the background page sends 'onHostedPageLoaded' message. | |
316 * @param {!Object} msg Details sent with the message. | |
317 */ | |
318 onAuthPageLoaded_: function(msg) { | |
319 if (msg.isSAMLPage && !this.isSAMLFlow_) { | |
320 // GAIA redirected to a SAML login page. The credentials provided to this | |
321 // page will determine what user gets logged in. The credentials obtained | |
322 // from the GAIA login form are no longer relevant and can be discarded. | |
323 this.isSAMLFlow_ = true; | |
324 this.email_ = null; | |
325 this.gaiaId_ = null; | |
326 this.passwordBytes_ = null; | |
327 } | |
328 | |
329 window.parent.postMessage({ | |
330 'method': 'authPageLoaded', | |
331 'isSAML': this.isSAMLFlow_, | |
332 'domain': extractDomain(msg.url) | |
333 }, this.parentPage_); | |
334 }, | |
335 | |
336 /** | |
337 * Invoked when the background page sends an 'onInsecureContentBlocked' | |
338 * message. | |
339 * @param {!Object} msg Details sent with the message. | |
340 */ | |
341 onInsecureContentBlocked_: function(msg) { | |
342 window.parent.postMessage({ | |
343 'method': 'insecureContentBlocked', | |
344 'url': stripParams(msg.url) | |
345 }, this.parentPage_); | |
346 }, | |
347 | |
348 /** | |
349 * Invoked when one of the credential passing API methods is called by a SAML | |
350 * provider. | |
351 * @param {!Object} msg Details of the API call. | |
352 */ | |
353 onAPICall_: function(msg) { | |
354 var call = msg.call; | |
355 if (call.method == 'initialize') { | |
356 if (!Number.isInteger(call.requestedVersion) || | |
357 call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) { | |
358 this.sendInitializationFailure_(); | |
359 return; | |
360 } | |
361 | |
362 this.apiVersion_ = Math.min(call.requestedVersion, | |
363 Authenticator.MAX_API_VERSION_VERSION); | |
364 this.initialized_ = true; | |
365 this.sendInitializationSuccess_(); | |
366 return; | |
367 } | |
368 | |
369 if (call.method == 'add') { | |
370 if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) { | |
371 console.error('Authenticator.onAPICall_: unsupported key type'); | |
372 return; | |
373 } | |
374 // Not setting |email_| and |gaiaId_| because this API call will | |
375 // eventually be followed by onCompleteLogin_() which does set it. | |
376 this.apiToken_ = call.token; | |
377 this.passwordBytes_ = call.passwordBytes; | |
378 } else if (call.method == 'confirm') { | |
379 if (call.token != this.apiToken_) | |
380 console.error('Authenticator.onAPICall_: token mismatch'); | |
381 } else { | |
382 console.error('Authenticator.onAPICall_: unknown message'); | |
383 } | |
384 }, | |
385 | |
386 onGotAuthCode_: function(authCode) { | |
387 window.parent.postMessage({ | |
388 'method': 'completeAuthenticationAuthCodeOnly', | |
389 'authCode': authCode | |
390 }, this.parentPage_); | |
391 }, | |
392 | |
393 sendInitializationSuccess_: function() { | |
394 this.supportChannel_.send({name: 'apiResponse', response: { | |
395 result: 'initialized', | |
396 version: this.apiVersion_, | |
397 keyTypes: Authenticator.API_KEY_TYPES | |
398 }}); | |
399 }, | |
400 | |
401 sendInitializationFailure_: function() { | |
402 this.supportChannel_.send({ | |
403 name: 'apiResponse', | |
404 response: {result: 'initialization_failed'} | |
405 }); | |
406 }, | |
407 | |
408 /** | |
409 * Callback invoked for 'completeLogin' message. | |
410 * @param {Object=} msg Message sent from background page. | |
411 */ | |
412 onCompleteLogin_: function(msg) { | |
413 if (!msg.email || !msg.gaiaId || !msg.sessionIndex) { | |
414 // On desktop, if the skipForNow message field is set, send it to handler. | |
415 // This does not require the email, gaiaid or session to be valid. | |
416 if (this.desktopMode_ && msg.skipForNow) { | |
417 this.completeLogin_(msg); | |
418 } else { | |
419 console.error('Missing fields to complete login.'); | |
420 window.parent.postMessage({method: 'missingGaiaInfo'}, | |
421 this.parentPage_); | |
422 return; | |
423 } | |
424 } | |
425 | |
426 // Skip SAML extra steps for desktop flow and non-SAML flow. | |
427 if (!this.isSAMLFlow_ || this.desktopMode_) { | |
428 this.completeLogin_(msg); | |
429 return; | |
430 } | |
431 | |
432 this.email_ = msg.email; | |
433 this.gaiaId_ = msg.gaiaId; | |
434 // Password from |msg| is not used because ChromeOS SAML flow | |
435 // gets password by asking user to confirm. | |
436 this.skipForNow_ = msg.skipForNow; | |
437 this.sessionIndex_ = msg.sessionIndex; | |
438 | |
439 if (this.passwordBytes_) { | |
440 // If the credentials passing API was used, login is complete. | |
441 window.parent.postMessage({method: 'samlApiUsed'}, this.parentPage_); | |
442 this.completeLogin_(msg); | |
443 } else if (!this.needPassword_) { | |
444 // If the credentials passing API was not used, the password was obtained | |
445 // by scraping. It must be verified before use. However, the host may not | |
446 // be interested in the password at all. In that case, verification is | |
447 // unnecessary and login is complete. | |
448 this.completeLogin_(msg); | |
449 } else { | |
450 this.supportChannel_.sendWithCallback( | |
451 {name: 'getScrapedPasswords'}, | |
452 function(passwords) { | |
453 if (passwords.length == 0) { | |
454 window.parent.postMessage( | |
455 {method: 'noPassword', email: this.email_}, | |
456 this.parentPage_); | |
457 } else { | |
458 window.parent.postMessage({method: 'confirmPassword', | |
459 email: this.email_, | |
460 passwordCount: passwords.length}, | |
461 this.parentPage_); | |
462 } | |
463 }.bind(this)); | |
464 } | |
465 }, | |
466 | |
467 onVerifyConfirmedPassword_: function(password) { | |
468 this.supportChannel_.sendWithCallback( | |
469 {name: 'getScrapedPasswords'}, | |
470 function(passwords) { | |
471 for (var i = 0; i < passwords.length; ++i) { | |
472 if (passwords[i] == password) { | |
473 this.passwordBytes_ = passwords[i]; | |
474 // SAML login is complete when the user has successfully | |
475 // confirmed the password. | |
476 if (this.passwordBytes_ !== null) | |
477 this.completeLogin_(); | |
478 return; | |
479 } | |
480 } | |
481 window.parent.postMessage( | |
482 {method: 'confirmPassword', email: this.email_}, | |
483 this.parentPage_); | |
484 }.bind(this)); | |
485 }, | |
486 | |
487 onMessage: function(e) { | |
488 var msg = e.data; | |
489 | |
490 if (this.useEafe_) { | |
491 if (msg == '!_{h:\'gaia-frame\'}' && this.isGaiaMessage_(e)) { | |
492 // Sends client ID again on the hello message to work around the SSO | |
493 // signin issue. | |
494 // TODO(xiyuan): Revisit this when EAFE is integrated or for webview. | |
495 $('gaia-frame').contentWindow.postMessage({ | |
496 clientId: this.clientId_ | |
497 }, this.gaiaUrl_); | |
498 } else if (typeof msg == 'object' && | |
499 msg.type == 'authorizationCode' && this.isGaiaMessage_(e)) { | |
500 this.onGotAuthCode_(msg.authorizationCode); | |
501 } else { | |
502 console.error('Authenticator.onMessage: unknown message' + | |
503 ', msg=' + JSON.stringify(msg)); | |
504 } | |
505 | |
506 return; | |
507 } | |
508 | |
509 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { | |
510 // At this point GAIA does not yet know the gaiaId, so its not set here. | |
511 this.email_ = msg.email; | |
512 this.passwordBytes_ = msg.password; | |
513 this.attemptToken_ = msg.attemptToken; | |
514 this.chooseWhatToSync_ = msg.chooseWhatToSync; | |
515 this.isSAMLFlow_ = false; | |
516 if (this.supportChannel_) | |
517 this.supportChannel_.send({name: 'startAuth'}); | |
518 else | |
519 console.error('Support channel is not initialized.'); | |
520 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { | |
521 if (!this.gaiaLoaded_) { | |
522 this.gaiaLoaded_ = true; | |
523 this.maybeInitialized_(); | |
524 } | |
525 this.email_ = null; | |
526 this.gaiaId_ = null; | |
527 this.sessionIndex_ = false; | |
528 this.passwordBytes_ = null; | |
529 this.attemptToken_ = null; | |
530 this.isSAMLFlow_ = false; | |
531 this.skipForNow_ = false; | |
532 this.chooseWhatToSync_ = false; | |
533 if (this.supportChannel_) { | |
534 this.supportChannel_.send({name: 'resetAuth'}); | |
535 // This message is for clearing saml properties in gaia_auth_host and | |
536 // oobe_screen_oauth_enrollment. | |
537 window.parent.postMessage({ | |
538 'method': 'resetAuthFlow', | |
539 }, this.parentPage_); | |
540 } | |
541 } else if (msg.method == 'verifyConfirmedPassword' && | |
542 this.isParentMessage_(e)) { | |
543 this.onVerifyConfirmedPassword_(msg.password); | |
544 } else if (msg.method == 'redirectToSignin' && | |
545 this.isParentMessage_(e)) { | |
546 $('gaia-frame').src = this.constructInitialFrameUrl_(); | |
547 } else { | |
548 console.error('Authenticator.onMessage: unknown message + origin!?'); | |
549 } | |
550 } | |
551 }; | |
552 | |
553 Authenticator.getInstance().initialize(); | |
OLD | NEW |