Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / main.js
blob1614fb9a5d53eb1da3daef3e617e5756c089baf4
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.
5 /**
6  * Authenticator class wraps the communications between Gaia and its host.
7  */
8 function Authenticator() {
11 /**
12  * Gaia auth extension url origin.
13  * @type {string}
14  */
15 Authenticator.THIS_EXTENSION_ORIGIN =
16     'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
18 /**
19  * The lowest version of the credentials passing API supported.
20  * @type {number}
21  */
22 Authenticator.MIN_API_VERSION_VERSION = 1;
24 /**
25  * The highest version of the credentials passing API supported.
26  * @type {number}
27  */
28 Authenticator.MAX_API_VERSION_VERSION = 1;
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',
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'
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_;
58 Authenticator.prototype = {
59   email_: null,
60   gaiaId_: null,
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,
69   needPassword_: false,
70   chooseWhatToSync_: false,
71   skipForNow_: false,
72   sessionIndex_: null,
73   attemptToken_: null,
75   // Input params from extension initialization URL.
76   inputLang_: undefined,
77   intputEmail_: undefined,
79   isSAMLFlow_: false,
80   gaiaLoaded_: false,
81   supportChannel_: null,
83   useEafe_: false,
84   clientId_: '',
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',
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);
100       var params = e.data;
101       params.parentPage = e.origin;
102       this.initializeFromParent_(params);
103       this.onPageLoad_();
104     }.bind(this);
106     document.addEventListener('DOMContentLoaded', function() {
107       window.addEventListener('message', handleInitializeMessage);
108       window.parent.postMessage({'method': 'loginUIDOMContentLoaded'}, '*');
109     });
110   },
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';
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_.indexOf('ServiceLogin') !== 0 ||
134         this.service_ !== 'chromeoslogin' ||
135         this.useEafe_;
136   },
138   isGaiaMessage_: function(msg) {
139     // Not quite right, but good enough.
140     return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
141            this.GAIA_URL.indexOf(msg.origin) == 0;
142   },
144   isParentMessage_: function(msg) {
145     return msg.origin == this.parentPage_;
146   },
148   constructInitialFrameUrl_: function() {
149     var url = this.gaiaUrl_ + this.gaiaPath_;
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   },
165   onPageLoad_: function() {
166     window.addEventListener('message', this.onMessage.bind(this), false);
167     this.initSupportChannel_();
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_();
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   },
190   initSupportChannel_: function() {
191     var supportChannel = new Channel();
192     supportChannel.connect('authMain');
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_;
200       if (this.supportChannel_) {
201         console.error('Support channel is already initialized.');
202         return;
203       }
204       this.supportChannel_ = supportChannel;
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         });
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));
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() {});
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   },
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   },
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   },
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   },
286   /**
287    * Invoked when support channel is connected.
288    */
289   initSAML_: function() {
290     this.isSAMLFlow_ = false;
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_.indexOf('https://') == 0) {
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   },
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     }
329     window.parent.postMessage({
330       'method': 'authPageLoaded',
331       'isSAML': this.isSAMLFlow_,
332       'domain': extractDomain(msg.url)
333     }, this.parentPage_);
334   },
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   },
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       }
362       this.apiVersion_ = Math.min(call.requestedVersion,
363                                   Authenticator.MAX_API_VERSION_VERSION);
364       this.initialized_ = true;
365       this.sendInitializationSuccess_();
366       return;
367     }
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   },
386   onGotAuthCode_: function(authCode) {
387     window.parent.postMessage({
388       'method': 'completeAuthenticationAuthCodeOnly',
389       'authCode': authCode
390     }, this.parentPage_);
391   },
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   },
401   sendInitializationFailure_: function() {
402     this.supportChannel_.send({
403       name: 'apiResponse',
404       response: {result: 'initialization_failed'}
405     });
406   },
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     }
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     }
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;
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   },
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   },
487   onMessage: function(e) {
488     var msg = e.data;
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       }
506       return;
507     }
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   }
553 Authenticator.getInstance().initialize();