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