f2c7f36e2f25af0ed4088877c9e5cb702090169a
[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  * Singleton getter of Authenticator.
20  * @return {Object} The singleton instance of Authenticator.
21  */
22 Authenticator.getInstance = function() {
23   if (!Authenticator.instance_) {
24     Authenticator.instance_ = new Authenticator();
25   }
26   return Authenticator.instance_;
27 };
28
29 Authenticator.prototype = {
30   email_: null,
31   password_: null,
32   attemptToken_: null,
33
34   // Input params from extension initialization URL.
35   inputLang_: undefined,
36   intputEmail_: undefined,
37
38   isSAMLFlow_: false,
39   samlSupportChannel_: null,
40
41   GAIA_URL: 'https://accounts.google.com/',
42   GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
43   PARENT_PAGE: 'chrome://oobe/',
44   SERVICE_ID: 'chromeoslogin',
45   CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
46   CONSTRAINED_FLOW_SOURCE: 'chrome',
47
48   initialize: function() {
49     var params = getUrlSearchParams(location.search);
50     this.parentPage_ = params.parentPage || this.PARENT_PAGE;
51     this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
52     this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
53     this.inputLang_ = params.hl;
54     this.inputEmail_ = params.email;
55     this.service_ = params.service || this.SERVICE_ID;
56     this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
57     this.continueUrlWithoutParams_ = stripParams(this.continueUrl_);
58     this.inlineMode_ = params.inlineMode == '1';
59     this.constrained_ = params.constrained == '1';
60     this.partitionId_ = params.partitionId || '';
61     this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
62     this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
63     this.loaded_ = false;
64
65     document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this));
66     document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
67   },
68
69   isGaiaMessage_: function(msg) {
70     // Not quite right, but good enough.
71     return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
72            this.GAIA_URL.indexOf(msg.origin) == 0;
73   },
74
75   isInternalMessage_: function(msg) {
76     return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
77   },
78
79   isParentMessage_: function(msg) {
80     return msg.origin == this.parentPage_;
81   },
82
83   constructInitialFrameUrl_: function() {
84     var url = this.gaiaUrl_ + this.gaiaPath_;
85
86     url = appendParam(url, 'service', this.service_);
87     url = appendParam(url, 'continue', this.continueUrl_);
88     if (this.inputLang_)
89       url = appendParam(url, 'hl', this.inputLang_);
90     if (this.inputEmail_)
91       url = appendParam(url, 'Email', this.inputEmail_);
92     if (this.constrained_)
93       url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
94     return url;
95   },
96
97   /** Callback when all loads in the gaia webview is complete. */
98   onWebviewLoadstop_: function(gaiaFrame) {
99     if (gaiaFrame.src.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
100       // Detect when login is finished by the load stop event of the continue
101       // URL. Cannot reuse the login complete flow in success.html, because
102       // webview does not support extension pages yet.
103       var skipForNow = false;
104       if (this.inlineMode_ && gaiaFrame.src.indexOf('ntp=1') >= 0) {
105         skipForNow = true;
106       }
107       msg = {
108         'method': 'completeLogin',
109         'skipForNow': skipForNow
110       };
111       window.parent.postMessage(msg, this.parentPage_);
112       // Do no report state to the parent for the continue URL, since it is a
113       // blank page.
114       return;
115     }
116
117     // Report the current state to the parent which will then update the
118     // browser history so that later it could respond properly to back/forward.
119     var msg = {
120       'method': 'reportState',
121       'src': gaiaFrame.src
122     };
123     window.parent.postMessage(msg, this.parentPage_);
124
125     if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
126       gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
127         // Send an initial message to gaia so that it has an JavaScript
128         // reference to the embedder.
129         gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
130       });
131       if (this.constrained_) {
132         var preventContextMenu = 'document.addEventListener("contextmenu", ' +
133                                  'function(e) {e.preventDefault();})';
134         gaiaFrame.executeScript({code: preventContextMenu});
135       }
136     }
137
138     this.loaded_ || this.onLoginUILoaded();
139   },
140
141   /**
142    * Callback when the gaia webview attempts to open a new window.
143    */
144   onWebviewNewWindow_: function(gaiaFrame, e) {
145     window.open(e.targetUrl, '_blank');
146     e.window.discard();
147   },
148
149   onWebviewRequestCompleted_: function(details) {
150     if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
151       return;
152     }
153
154     var headers = details.responseHeaders;
155     for (var i = 0; headers && i < headers.length; ++i) {
156       if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
157         return;
158       }
159     }
160     var msg = {
161       'method': 'switchToFullTab',
162       'url': details.url
163     };
164     window.parent.postMessage(msg, this.parentPage_);
165   },
166
167   loadFrame_: function() {
168     var gaiaFrame = $('gaia-frame');
169     gaiaFrame.partition = this.partitionId_;
170     gaiaFrame.src = this.initialFrameUrl_;
171     if (this.inlineMode_) {
172       gaiaFrame.addEventListener(
173           'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame));
174       gaiaFrame.addEventListener(
175           'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame));
176     }
177     if (this.constrained_) {
178       gaiaFrame.request.onCompleted.addListener(
179           this.onWebviewRequestCompleted_.bind(this),
180           {urls: ['<all_urls>'], types: ['main_frame']},
181           ['responseHeaders']);
182     }
183   },
184
185   completeLogin: function() {
186     var msg = {
187       'method': 'completeLogin',
188       'email': this.email_,
189       'password': this.password_,
190       'usingSAML': this.isSAMLFlow_
191     };
192     window.parent.postMessage(msg, this.parentPage_);
193     if (this.samlSupportChannel_)
194       this.samlSupportChannel_.send({name: 'resetAuth'});
195   },
196
197   onPageLoad: function(e) {
198     window.addEventListener('message', this.onMessage.bind(this), false);
199     this.loadFrame_();
200   },
201
202   /**
203    * Invoked when 'enableSAML' event is received to initialize SAML support.
204    */
205   onEnableSAML_: function() {
206     this.isSAMLFlow_ = false;
207
208     this.samlSupportChannel_ = new Channel();
209     this.samlSupportChannel_.connect('authMain');
210     this.samlSupportChannel_.registerMessage(
211         'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
212     this.samlSupportChannel_.registerMessage(
213         'apiCall', this.onAPICall_.bind(this));
214     this.samlSupportChannel_.send({
215       name: 'setGaiaUrl',
216       gaiaUrl: this.gaiaUrl_
217     });
218   },
219
220   /**
221    * Invoked when the background page sends 'onHostedPageLoaded' message.
222    * @param {!Object} msg Details sent with the message.
223    */
224   onAuthPageLoaded_: function(msg) {
225     var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
226
227     if (isSAMLPage && !this.isSAMLFlow_) {
228       // GAIA redirected to a SAML login page. The credentials provided to this
229       // page will determine what user gets logged in. The credentials obtained
230       // from the GAIA login from are no longer relevant and can be discarded.
231       this.isSAMLFlow_ = true;
232       this.email_ = null;
233       this.password_ = null;
234     }
235
236     window.parent.postMessage({
237       'method': 'authPageLoaded',
238       'isSAML': this.isSAMLFlow_,
239       'domain': extractDomain(msg.url)
240     }, this.parentPage_);
241   },
242
243   /**
244    * Invoked when one of the credential passing API methods is called by a SAML
245    * provider.
246    * @param {!Object} msg Details of the API call.
247    */
248   onAPICall_: function(msg) {
249     var call = msg.call;
250     if (call.method == 'add') {
251       this.apiToken_ = call.token;
252       this.email_ = call.user;
253       this.password_ = call.password;
254     } else if (call.method == 'confirm') {
255       if (call.token != this.apiToken_)
256         console.error('Authenticator.onAPICall_: token mismatch');
257     } else {
258       console.error('Authenticator.onAPICall_: unknown message');
259     }
260   },
261
262   onLoginUILoaded: function() {
263     var msg = {
264       'method': 'loginUILoaded'
265     };
266     window.parent.postMessage(msg, this.parentPage_);
267     if (this.inlineMode_) {
268       // TODO(guohui): temporary workaround until webview team fixes the focus
269       // on their side.
270       var gaiaFrame = $('gaia-frame');
271       gaiaFrame.focus();
272       gaiaFrame.onblur = function() {
273         gaiaFrame.focus();
274       };
275     }
276     this.loaded_ = true;
277   },
278
279   onConfirmLogin_: function() {
280     if (!this.isSAMLFlow_) {
281       this.completeLogin();
282       return;
283     }
284
285     // Retrieve the e-mail address of the user who just authenticated from GAIA.
286     window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
287                                attemptToken: this.attemptToken_},
288                               this.parentPage_);
289
290     if (!this.password_) {
291       this.samlSupportChannel_.sendWithCallback(
292           {name: 'getScrapedPasswords'},
293           function(passwords) {
294             if (passwords.length == 0) {
295               window.parent.postMessage(
296                   {method: 'noPassword', email: this.email_},
297                   this.parentPage_);
298             } else {
299               window.parent.postMessage(
300                   {method: 'confirmPassword', email: this.email_},
301                   this.parentPage_);
302             }
303           }.bind(this));
304     }
305   },
306
307   maybeCompleteSAMLLogin_: function() {
308     // SAML login is complete when the user's e-mail address has been retrieved
309     // from GAIA and the user has successfully confirmed the password.
310     if (this.email_ !== null && this.password_ !== null)
311       this.completeLogin();
312   },
313
314   onVerifyConfirmedPassword_: function(password) {
315     this.samlSupportChannel_.sendWithCallback(
316         {name: 'getScrapedPasswords'},
317         function(passwords) {
318           for (var i = 0; i < passwords.length; ++i) {
319             if (passwords[i] == password) {
320               this.password_ = passwords[i];
321               this.maybeCompleteSAMLLogin_();
322               return;
323             }
324           }
325           window.parent.postMessage(
326               {method: 'confirmPassword', email: this.email_},
327               this.parentPage_);
328         }.bind(this));
329   },
330
331   onMessage: function(e) {
332     var msg = e.data;
333     if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
334       this.email_ = msg.email;
335       this.password_ = msg.password;
336       this.attemptToken_ = msg.attemptToken;
337       this.isSAMLFlow_ = false;
338       if (this.samlSupportChannel_)
339         this.samlSupportChannel_.send({name: 'startAuth'});
340     } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
341       this.email_ = null;
342       this.password_ = null;
343       this.attemptToken_ = null;
344       this.isSAMLFlow_ = false;
345       this.onLoginUILoaded();
346       if (this.samlSupportChannel_)
347         this.samlSupportChannel_.send({name: 'resetAuth'});
348     } else if (msg.method == 'setAuthenticatedUserEmail' &&
349                this.isParentMessage_(e)) {
350       if (this.attemptToken_ == msg.attemptToken) {
351         this.email_ = msg.email;
352         this.maybeCompleteSAMLLogin_();
353       }
354     } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
355       if (this.attemptToken_ == msg.attemptToken)
356         this.onConfirmLogin_();
357       else
358         console.error('Authenticator.onMessage: unexpected attemptToken!?');
359     } else if (msg.method == 'verifyConfirmedPassword' &&
360                this.isParentMessage_(e)) {
361       this.onVerifyConfirmedPassword_(msg.password);
362     } else if (msg.method == 'navigate' &&
363                this.isParentMessage_(e)) {
364       $('gaia-frame').src = msg.src;
365     } else if (msg.method == 'redirectToSignin' &&
366                this.isParentMessage_(e)) {
367       $('gaia-frame').src = this.constructInitialFrameUrl_();
368     } else {
369        console.error('Authenticator.onMessage: unknown message + origin!?');
370     }
371   }
372 };
373
374 Authenticator.getInstance().initialize();