Cast: Stop logging kVideoFrameSentToEncoder and rename a couple events.
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / main.js
blob3b6516b5fa9a608625df2c9ba2792816098a46df
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 = 2;
30 /**
31  * The key types supported for credentials passing API 2 and higher.
32  * @type {Array} Array of strings.
33  */
34 Authenticator.API_KEY_TYPES = [
35   'KEY_TYPE_PASSWORD_PLAIN',
38 /**
39  * Singleton getter of Authenticator.
40  * @return {Object} The singleton instance of Authenticator.
41  */
42 Authenticator.getInstance = function() {
43   if (!Authenticator.instance_) {
44     Authenticator.instance_ = new Authenticator();
45   }
46   return Authenticator.instance_;
49 Authenticator.prototype = {
50   email_: null,
52   // Depending on the key type chosen, this will contain the plain text password
53   // or a credential derived from it along with the information required to
54   // repeat the derivation, such as a salt. The information will be encoded so
55   // that it contains printable ASCII characters only. The exact encoding is TBD
56   // when support for key types other than plain text password is added.
57   passwordBytes_: null,
59   attemptToken_: null,
61   // Input params from extension initialization URL.
62   inputLang_: undefined,
63   intputEmail_: undefined,
65   isSAMLFlow_: false,
66   isSAMLEnabled_: false,
67   supportChannel_: null,
69   GAIA_URL: 'https://accounts.google.com/',
70   GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
71   PARENT_PAGE: 'chrome://oobe/',
72   SERVICE_ID: 'chromeoslogin',
73   CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
74   CONSTRAINED_FLOW_SOURCE: 'chrome',
76   initialize: function() {
77     var params = getUrlSearchParams(location.search);
78     this.parentPage_ = params.parentPage || this.PARENT_PAGE;
79     this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
80     this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
81     this.inputLang_ = params.hl;
82     this.inputEmail_ = params.email;
83     this.service_ = params.service || this.SERVICE_ID;
84     this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
85     this.desktopMode_ = params.desktopMode == '1';
86     this.isConstrainedWindow_ = params.constrained == '1';
87     this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
88     this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
90     document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
91     if (!this.desktopMode_) {
92       // SAML is always enabled in desktop mode, thus no need to listen for
93       // enableSAML event.
94       document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
95     }
96   },
98   isGaiaMessage_: function(msg) {
99     // Not quite right, but good enough.
100     return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
101            this.GAIA_URL.indexOf(msg.origin) == 0;
102   },
104   isInternalMessage_: function(msg) {
105     return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
106   },
108   isParentMessage_: function(msg) {
109     return msg.origin == this.parentPage_;
110   },
112   constructInitialFrameUrl_: function() {
113     var url = this.gaiaUrl_ + this.gaiaPath_;
115     url = appendParam(url, 'service', this.service_);
116     url = appendParam(url, 'continue', this.continueUrl_);
117     if (this.inputLang_)
118       url = appendParam(url, 'hl', this.inputLang_);
119     if (this.inputEmail_)
120       url = appendParam(url, 'Email', this.inputEmail_);
121     if (this.isConstrainedWindow_)
122       url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
123     return url;
124   },
126   onPageLoad_: function() {
127     window.addEventListener('message', this.onMessage.bind(this), false);
129     var gaiaFrame = $('gaia-frame');
130     gaiaFrame.src = this.initialFrameUrl_;
132     if (this.desktopMode_) {
133       var handler = function() {
134         this.onLoginUILoaded_();
135         gaiaFrame.removeEventListener('load', handler);
137         this.initDesktopChannel_();
138       }.bind(this);
139       gaiaFrame.addEventListener('load', handler);
140     }
141   },
143   initDesktopChannel_: function() {
144     this.supportChannel_ = new Channel();
145     this.supportChannel_.connect('authMain');
147     var channelConnected = false;
148     this.supportChannel_.registerMessage('channelConnected', function() {
149       channelConnected = true;
151       this.supportChannel_.send({
152         name: 'initDesktopFlow',
153         gaiaUrl: this.gaiaUrl_,
154         continueUrl: stripParams(this.continueUrl_),
155         isConstrainedWindow: this.isConstrainedWindow_
156       });
157       this.supportChannel_.registerMessage(
158           'switchToFullTab', this.switchToFullTab_.bind(this));
159       this.supportChannel_.registerMessage(
160           'completeLogin', this.completeLogin_.bind(this));
162       this.onEnableSAML_();
163     }.bind(this));
165     window.setTimeout(function() {
166       if (!channelConnected) {
167         // Re-initialize the channel if it is not connected properly, e.g.
168         // connect may be called before background script started running.
169         this.initDesktopChannel_();
170       }
171     }.bind(this), 200);
172   },
174   /**
175    * Invoked when the login UI is initialized or reset.
176    */
177   onLoginUILoaded_: function() {
178     var msg = {
179       'method': 'loginUILoaded'
180     };
181     window.parent.postMessage(msg, this.parentPage_);
182   },
184   /**
185    * Invoked when the background script sends a message to indicate that the
186    * current content does not fit in a constrained window.
187    * @param {Object=} opt_extraMsg Optional extra info to send.
188    */
189   switchToFullTab_: function(msg) {
190     var parentMsg = {
191       'method': 'switchToFullTab',
192       'url': msg.url
193     };
194     window.parent.postMessage(parentMsg, this.parentPage_);
195   },
197   /**
198    * Invoked when the signin flow is complete.
199    * @param {Object=} opt_extraMsg Optional extra info to send.
200    */
201   completeLogin_: function(opt_extraMsg) {
202     var msg = {
203       'method': 'completeLogin',
204       'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
205       'password': (opt_extraMsg && opt_extraMsg.password) ||
206                   this.passwordBytes_,
207       'usingSAML': this.isSAMLFlow_,
208       'chooseWhatToSync': this.chooseWhatToSync_ || false,
209       'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
210       'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
211     };
212     window.parent.postMessage(msg, this.parentPage_);
213     if (this.isSAMLEnabled_)
214       this.supportChannel_.send({name: 'resetAuth'});
215   },
217   /**
218    * Invoked when 'enableSAML' event is received to initialize SAML support on
219    * Chrome OS, or when initDesktopChannel_ is called on desktop.
220    */
221   onEnableSAML_: function() {
222     this.isSAMLEnabled_ = true;
223     this.isSAMLFlow_ = false;
225     if (!this.supportChannel_) {
226       this.supportChannel_ = new Channel();
227       this.supportChannel_.connect('authMain');
228     }
230     this.supportChannel_.registerMessage(
231         'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
232     this.supportChannel_.registerMessage(
233         'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
234     this.supportChannel_.registerMessage(
235         'apiCall', this.onAPICall_.bind(this));
236     this.supportChannel_.send({
237       name: 'setGaiaUrl',
238       gaiaUrl: this.gaiaUrl_
239     });
240     if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
241       // Abort the login flow when content served over an unencrypted connection
242       // is detected on Chrome OS. This does not apply to tests that explicitly
243       // set a non-https GAIA URL and want to perform all authentication over
244       // http.
245       this.supportChannel_.send({
246         name: 'setBlockInsecureContent',
247         blockInsecureContent: true
248       });
249     }
250   },
252   /**
253    * Invoked when the background page sends 'onHostedPageLoaded' message.
254    * @param {!Object} msg Details sent with the message.
255    */
256   onAuthPageLoaded_: function(msg) {
257     var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
259     if (isSAMLPage && !this.isSAMLFlow_) {
260       // GAIA redirected to a SAML login page. The credentials provided to this
261       // page will determine what user gets logged in. The credentials obtained
262       // from the GAIA login form are no longer relevant and can be discarded.
263       this.isSAMLFlow_ = true;
264       this.email_ = null;
265       this.passwordBytes_ = null;
266     }
268     window.parent.postMessage({
269       'method': 'authPageLoaded',
270       'isSAML': this.isSAMLFlow_,
271       'domain': extractDomain(msg.url)
272     }, this.parentPage_);
273   },
275   /**
276    * Invoked when the background page sends an 'onInsecureContentBlocked'
277    * message.
278    */
279   onInsecureContentBlocked_: function() {
280     window.parent.postMessage({'method': 'insecureContentBlocked'},
281                               this.parentPage_);
282   },
284   /**
285    * Invoked when one of the credential passing API methods is called by a SAML
286    * provider.
287    * @param {!Object} msg Details of the API call.
288    */
289   onAPICall_: function(msg) {
290     var call = msg.call;
291     if (call.method == 'initialize') {
292       // TODO(bartfab): There was no |requestedVersion| parameter in version 1
293       // of the API. Remove this code once all consumers have switched to
294       // version 2 or higher.
295       if (!call.hasOwnProperty('requestedVersion')) {
296         if (Authenticator.MIN_API_VERSION_VERSION == 1) {
297           this.apiVersion_ = 1;
298           this.initialized_ = true;
299           this.sendInitializationSuccess_();
300         }
301         // The glue code for API version 1 interprets all responses as success.
302         // Instead of reporting failure, do not send any response at all.
303         return;
304       }
306       if (!Number.isInteger(call.requestedVersion) ||
307           call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
308         this.sendInitializationFailure_();
309         return;
310       }
312       this.apiVersion_ = Math.min(call.requestedVersion,
313                                   Authenticator.MAX_API_VERSION_VERSION);
314       this.initialized_ = true;
315       this.sendInitializationSuccess_();
316       return;
317     }
319     if (call.method == 'add') {
320       if (this.apiVersion_ > 1 &&
321           Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
322         console.error('Authenticator.onAPICall_: unsupported key type');
323         return;
324       }
325       this.apiToken_ = call.token;
326       this.email_ = call.user;
327       if (this.apiVersion_ == 1)
328         this.passwordBytes_ = call.password;
329       else
330         this.passwordBytes_ = call.passwordBytes;
331     } else if (call.method == 'confirm') {
332       if (call.token != this.apiToken_)
333         console.error('Authenticator.onAPICall_: token mismatch');
334     } else {
335       console.error('Authenticator.onAPICall_: unknown message');
336     }
337   },
339   sendInitializationSuccess_: function() {
340     var response = {
341       result: 'initialized',
342       version: this.apiVersion_
343     };
344     if (this.apiVersion_ >= 2)
345       response['keyTypes'] = Authenticator.API_KEY_TYPES;
347     this.supportChannel_.send({name: 'apiResponse', response: response});
348   },
350   sendInitializationFailure_: function() {
351     this.supportChannel_.send({
352       name: 'apiResponse',
353       response: {result: 'initialization_failed'}
354     });
355   },
357   onConfirmLogin_: function() {
358     if (!this.isSAMLFlow_) {
359       this.completeLogin_();
360       return;
361     }
363     var apiUsed = !!this.passwordBytes_;
365     // Retrieve the e-mail address of the user who just authenticated from GAIA.
366     window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
367                                attemptToken: this.attemptToken_,
368                                apiUsed: apiUsed},
369                               this.parentPage_);
371     if (!apiUsed) {
372       this.supportChannel_.sendWithCallback(
373           {name: 'getScrapedPasswords'},
374           function(passwords) {
375             if (passwords.length == 0) {
376               window.parent.postMessage(
377                   {method: 'noPassword', email: this.email_},
378                   this.parentPage_);
379             } else {
380               window.parent.postMessage({method: 'confirmPassword',
381                                          email: this.email_,
382                                          passwordCount: passwords.length},
383                                         this.parentPage_);
384             }
385           }.bind(this));
386     }
387   },
389   maybeCompleteSAMLLogin_: function() {
390     // SAML login is complete when the user's e-mail address has been retrieved
391     // from GAIA and the user has successfully confirmed the password.
392     if (this.email_ !== null && this.passwordBytes_ !== null)
393       this.completeLogin_();
394   },
396   onVerifyConfirmedPassword_: function(password) {
397     this.supportChannel_.sendWithCallback(
398         {name: 'getScrapedPasswords'},
399         function(passwords) {
400           for (var i = 0; i < passwords.length; ++i) {
401             if (passwords[i] == password) {
402               this.passwordBytes_ = passwords[i];
403               this.maybeCompleteSAMLLogin_();
404               return;
405             }
406           }
407           window.parent.postMessage(
408               {method: 'confirmPassword', email: this.email_},
409               this.parentPage_);
410         }.bind(this));
411   },
413   onMessage: function(e) {
414     var msg = e.data;
415     if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
416       this.email_ = msg.email;
417       this.passwordBytes_ = msg.password;
418       this.attemptToken_ = msg.attemptToken;
419       this.chooseWhatToSync_ = msg.chooseWhatToSync;
420       this.isSAMLFlow_ = false;
421       if (this.isSAMLEnabled_)
422         this.supportChannel_.send({name: 'startAuth'});
423     } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
424       this.email_ = null;
425       this.passwordBytes_ = null;
426       this.attemptToken_ = null;
427       this.isSAMLFlow_ = false;
428       this.onLoginUILoaded_();
429       if (this.isSAMLEnabled_)
430         this.supportChannel_.send({name: 'resetAuth'});
431     } else if (msg.method == 'setAuthenticatedUserEmail' &&
432                this.isParentMessage_(e)) {
433       if (this.attemptToken_ == msg.attemptToken) {
434         this.email_ = msg.email;
435         this.maybeCompleteSAMLLogin_();
436       }
437     } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
438       if (this.attemptToken_ == msg.attemptToken)
439         this.onConfirmLogin_();
440       else
441         console.error('Authenticator.onMessage: unexpected attemptToken!?');
442     } else if (msg.method == 'verifyConfirmedPassword' &&
443                this.isParentMessage_(e)) {
444       this.onVerifyConfirmedPassword_(msg.password);
445     } else if (msg.method == 'redirectToSignin' &&
446                this.isParentMessage_(e)) {
447       $('gaia-frame').src = this.constructInitialFrameUrl_();
448     } else {
449        console.error('Authenticator.onMessage: unknown message + origin!?');
450     }
451   }
454 Authenticator.getInstance().initialize();