Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / gaia_auth / main.js
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  * 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_;
47 };
48
49 Authenticator.prototype = {
50   email_: null,
51
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,
58
59   attemptToken_: null,
60
61   // Input params from extension initialization URL.
62   inputLang_: undefined,
63   intputEmail_: undefined,
64
65   isSAMLFlow_: false,
66   isSAMLEnabled_: false,
67   supportChannel_: null,
68
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',
75
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_);
89
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   },
97
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   },
103
104   isInternalMessage_: function(msg) {
105     return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
106   },
107
108   isParentMessage_: function(msg) {
109     return msg.origin == this.parentPage_;
110   },
111
112   constructInitialFrameUrl_: function() {
113     var url = this.gaiaUrl_ + this.gaiaPath_;
114
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   },
125
126   onPageLoad_: function() {
127     window.addEventListener('message', this.onMessage.bind(this), false);
128
129     var gaiaFrame = $('gaia-frame');
130     gaiaFrame.src = this.initialFrameUrl_;
131
132     if (this.desktopMode_) {
133       var handler = function() {
134         this.onLoginUILoaded_();
135         gaiaFrame.removeEventListener('load', handler);
136
137         this.initDesktopChannel_();
138       }.bind(this);
139       gaiaFrame.addEventListener('load', handler);
140     }
141   },
142
143   initDesktopChannel_: function() {
144     this.supportChannel_ = new Channel();
145     this.supportChannel_.connect('authMain');
146
147     var channelConnected = false;
148     this.supportChannel_.registerMessage('channelConnected', function() {
149       channelConnected = true;
150
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));
161
162       this.onEnableSAML_();
163     }.bind(this));
164
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   },
173
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   },
183
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   },
196
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   },
216
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;
224
225     if (!this.supportChannel_) {
226       this.supportChannel_ = new Channel();
227       this.supportChannel_.connect('authMain');
228     }
229
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   },
251
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;
258
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     }
267
268     window.parent.postMessage({
269       'method': 'authPageLoaded',
270       'isSAML': this.isSAMLFlow_,
271       'domain': extractDomain(msg.url)
272     }, this.parentPage_);
273   },
274
275   /**
276    * Invoked when the background page sends an 'onInsecureContentBlocked'
277    * message.
278    * @param {!Object} msg Details sent with the message.
279    */
280   onInsecureContentBlocked_: function(msg) {
281     window.parent.postMessage({
282       'method': 'insecureContentBlocked',
283       'url': msg.url
284     }, this.parentPage_);
285   },
286
287   /**
288    * Invoked when one of the credential passing API methods is called by a SAML
289    * provider.
290    * @param {!Object} msg Details of the API call.
291    */
292   onAPICall_: function(msg) {
293     var call = msg.call;
294     if (call.method == 'initialize') {
295       if (!Number.isInteger(call.requestedVersion) ||
296           call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
297         this.sendInitializationFailure_();
298         return;
299       }
300
301       this.apiVersion_ = Math.min(call.requestedVersion,
302                                   Authenticator.MAX_API_VERSION_VERSION);
303       this.initialized_ = true;
304       this.sendInitializationSuccess_();
305       return;
306     }
307
308     if (call.method == 'add') {
309       if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
310         console.error('Authenticator.onAPICall_: unsupported key type');
311         return;
312       }
313       this.apiToken_ = call.token;
314       this.email_ = call.user;
315       this.passwordBytes_ = call.passwordBytes;
316     } else if (call.method == 'confirm') {
317       if (call.token != this.apiToken_)
318         console.error('Authenticator.onAPICall_: token mismatch');
319     } else {
320       console.error('Authenticator.onAPICall_: unknown message');
321     }
322   },
323
324   sendInitializationSuccess_: function() {
325     this.supportChannel_.send({name: 'apiResponse', response: {
326       result: 'initialized',
327       version: this.apiVersion_,
328       keyTypes: Authenticator.API_KEY_TYPES
329     }});
330   },
331
332   sendInitializationFailure_: function() {
333     this.supportChannel_.send({
334       name: 'apiResponse',
335       response: {result: 'initialization_failed'}
336     });
337   },
338
339   onConfirmLogin_: function() {
340     if (!this.isSAMLFlow_) {
341       this.completeLogin_();
342       return;
343     }
344
345     var apiUsed = !!this.passwordBytes_;
346
347     // Retrieve the e-mail address of the user who just authenticated from GAIA.
348     window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
349                                attemptToken: this.attemptToken_,
350                                apiUsed: apiUsed},
351                               this.parentPage_);
352
353     if (!apiUsed) {
354       this.supportChannel_.sendWithCallback(
355           {name: 'getScrapedPasswords'},
356           function(passwords) {
357             if (passwords.length == 0) {
358               window.parent.postMessage(
359                   {method: 'noPassword', email: this.email_},
360                   this.parentPage_);
361             } else {
362               window.parent.postMessage({method: 'confirmPassword',
363                                          email: this.email_,
364                                          passwordCount: passwords.length},
365                                         this.parentPage_);
366             }
367           }.bind(this));
368     }
369   },
370
371   maybeCompleteSAMLLogin_: function() {
372     // SAML login is complete when the user's e-mail address has been retrieved
373     // from GAIA and the user has successfully confirmed the password.
374     if (this.email_ !== null && this.passwordBytes_ !== null)
375       this.completeLogin_();
376   },
377
378   onVerifyConfirmedPassword_: function(password) {
379     this.supportChannel_.sendWithCallback(
380         {name: 'getScrapedPasswords'},
381         function(passwords) {
382           for (var i = 0; i < passwords.length; ++i) {
383             if (passwords[i] == password) {
384               this.passwordBytes_ = passwords[i];
385               this.maybeCompleteSAMLLogin_();
386               return;
387             }
388           }
389           window.parent.postMessage(
390               {method: 'confirmPassword', email: this.email_},
391               this.parentPage_);
392         }.bind(this));
393   },
394
395   onMessage: function(e) {
396     var msg = e.data;
397     if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
398       this.email_ = msg.email;
399       this.passwordBytes_ = msg.password;
400       this.attemptToken_ = msg.attemptToken;
401       this.chooseWhatToSync_ = msg.chooseWhatToSync;
402       this.isSAMLFlow_ = false;
403       if (this.isSAMLEnabled_)
404         this.supportChannel_.send({name: 'startAuth'});
405     } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
406       this.email_ = null;
407       this.passwordBytes_ = null;
408       this.attemptToken_ = null;
409       this.isSAMLFlow_ = false;
410       this.onLoginUILoaded_();
411       if (this.isSAMLEnabled_)
412         this.supportChannel_.send({name: 'resetAuth'});
413     } else if (msg.method == 'setAuthenticatedUserEmail' &&
414                this.isParentMessage_(e)) {
415       if (this.attemptToken_ == msg.attemptToken) {
416         this.email_ = msg.email;
417         this.maybeCompleteSAMLLogin_();
418       }
419     } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
420       if (this.attemptToken_ == msg.attemptToken)
421         this.onConfirmLogin_();
422       else
423         console.error('Authenticator.onMessage: unexpected attemptToken!?');
424     } else if (msg.method == 'verifyConfirmedPassword' &&
425                this.isParentMessage_(e)) {
426       this.onVerifyConfirmedPassword_(msg.password);
427     } else if (msg.method == 'redirectToSignin' &&
428                this.isParentMessage_(e)) {
429       $('gaia-frame').src = this.constructInitialFrameUrl_();
430     } else {
431        console.error('Authenticator.onMessage: unknown message + origin!?');
432     }
433   }
434 };
435
436 Authenticator.getInstance().initialize();