ChromeOS Gaia: SAML password confirmation dialog
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth_host / authenticator.js
blob020493cdf46cd1ccf7be7b5a82c5a6735841a8bc
1 // Copyright 2014 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 <include src="saml_handler.js">
7 /**
8  * @fileoverview An UI component to authenciate to Chrome. The component hosts
9  * IdP web pages in a webview. A client who is interested in monitoring
10  * authentication events should pass a listener object of type
11  * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization,
12  * call {@code load} to start the authentication flow.
13  */
15 cr.define('cr.login', function() {
16   'use strict';
18   // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead
19   // of hardcoding the prod URL here.  As is, this does not work with staging
20   // environments.
21   var IDP_ORIGIN = 'https://accounts.google.com/';
22   var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide';
23   var CONTINUE_URL =
24       'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html';
25   var SIGN_IN_HEADER = 'google-accounts-signin';
26   var EMBEDDED_FORM_HEADER = 'google-accounts-embedded';
27   var LOCATION_HEADER = 'location';
28   var SET_COOKIE_HEADER = 'set-cookie';
29   var OAUTH_CODE_COOKIE = 'oauth_code';
30   var SERVICE_ID = 'chromeoslogin';
31   var EMBEDDED_SETUP_CHROMEOS_ENDPOINT = 'embedded/setup/chromeos';
32   var X_DEVICE_ID_HEADER = 'X-Device-ID';
33   var EPHEMERAL_DEVICE_ID_PREFIX = 't_';
35   /**
36    * The source URL parameter for the constrained signin flow.
37    */
38   var CONSTRAINED_FLOW_SOURCE = 'chrome';
40   /**
41    * Enum for the authorization mode, must match AuthMode defined in
42    * chrome/browser/ui/webui/inline_login_ui.cc.
43    * @enum {number}
44    */
45   var AuthMode = {
46     DEFAULT: 0,
47     OFFLINE: 1,
48     DESKTOP: 2
49   };
51   /**
52    * Enum for the authorization type.
53    * @enum {number}
54    */
55   var AuthFlow = {
56     DEFAULT: 0,
57     SAML: 1
58   };
60   /**
61    * Supported Authenticator params.
62    * @type {!Array<string>}
63    * @const
64    */
65   var SUPPORTED_PARAMS = [
66     'gaiaId',        // Obfuscated GAIA ID to skip the email prompt page
67                      // during the re-auth flow.
68     'gaiaUrl',       // Gaia url to use.
69     'gaiaPath',      // Gaia path to use without a leading slash.
70     'hl',            // Language code for the user interface.
71     'email',         // Pre-fill the email field in Gaia UI.
72     'service',       // Name of Gaia service.
73     'continueUrl',   // Continue url to use.
74     'frameUrl',      // Initial frame URL to use. If empty defaults to
75                      // gaiaUrl.
76     'constrained',   // Whether the extension is loaded in a constrained
77                      // window.
78     'clientId',      // Chrome client id.
79     'useEafe',       // Whether to use EAFE.
80     'needPassword',  // Whether the host is interested in getting a password.
81                      // If this set to |false|, |confirmPasswordCallback| is
82                      // not called before dispatching |authCopleted|.
83                      // Default is |true|.
84     'flow',          // One of 'default', 'enterprise', or 'theftprotection'.
85     'enterpriseDomain',    // Domain in which hosting device is (or should be)
86                            // enrolled.
87     'emailDomain',         // Value used to prefill domain for email.
88     'deviceId',            // User device ID (sync Id).
89     'sessionIsEphemeral',  // User session would be ephemeral.
90     'clientVersion',       // Version of the Chrome build.
91     'platformVersion',     // Version of the OS build.
92     'releaseChannel',      // Installation channel.
93     'endpointGen',         // Current endpoint generation.
94   ];
96   /**
97    * Initializes the authenticator component.
98    * @param {webview|string} webview The webview element or its ID to host IdP
99    *     web pages.
100    * @constructor
101    */
102   function Authenticator(webview) {
103     this.webview_ = typeof webview == 'string' ? $(webview) : webview;
104     assert(this.webview_);
106     this.email_ = null;
107     this.password_ = null;
108     this.gaiaId_ = null,
109     this.sessionIndex_ = null;
110     this.chooseWhatToSync_ = false;
111     this.skipForNow_ = false;
112     this.authFlow = AuthFlow.DEFAULT;
113     this.authDomain = '';
114     this.loaded_ = false;
115     this.idpOrigin_ = null;
116     this.continueUrl_ = null;
117     this.continueUrlWithoutParams_ = null;
118     this.initialFrameUrl_ = null;
119     this.reloadUrl_ = null;
120     this.trusted_ = true;
121     this.oauth_code_ = null;
122     this.deviceId_ = null;
123     this.sessionIsEphemeral_ = null;
124     this.onBeforeSetHeadersSet_ = false;
126     this.useEafe_ = false;
127     this.clientId_ = null;
129     this.samlHandler_ = new cr.login.SamlHandler(this.webview_);
130     this.confirmPasswordCallback = null;
131     this.noPasswordCallback = null;
132     this.insecureContentBlockedCallback = null;
133     this.samlApiUsedCallback = null;
134     this.missingGaiaInfoCallback = null;
135     this.needPassword = true;
136     this.samlHandler_.addEventListener(
137         'insecureContentBlocked',
138         this.onInsecureContentBlocked_.bind(this));
139     this.samlHandler_.addEventListener(
140         'authPageLoaded',
141         this.onAuthPageLoaded_.bind(this));
143     this.webview_.addEventListener('droplink', this.onDropLink_.bind(this));
144     this.webview_.addEventListener(
145         'newwindow', this.onNewWindow_.bind(this));
146     this.webview_.addEventListener(
147         'contentload', this.onContentLoad_.bind(this));
148     this.webview_.addEventListener(
149         'loadabort', this.onLoadAbort_.bind(this));
150     this.webview_.addEventListener(
151         'loadstop', this.onLoadStop_.bind(this));
152     this.webview_.addEventListener(
153         'loadcommit', this.onLoadCommit_.bind(this));
154     this.webview_.request.onCompleted.addListener(
155         this.onRequestCompleted_.bind(this),
156         {urls: ['<all_urls>'], types: ['main_frame']},
157         ['responseHeaders']);
158     this.webview_.request.onHeadersReceived.addListener(
159         this.onHeadersReceived_.bind(this),
160         {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
161         ['responseHeaders']);
162     window.addEventListener(
163         'message', this.onMessageFromWebview_.bind(this), false);
164     window.addEventListener(
165         'focus', this.onFocus_.bind(this), false);
166     window.addEventListener(
167         'popstate', this.onPopState_.bind(this), false);
168   }
170   Authenticator.prototype = Object.create(cr.EventTarget.prototype);
172   /**
173    * Reinitializes authentication parameters so that a failed login attempt
174    * would not result in an infinite loop.
175    */
176   Authenticator.prototype.clearCredentials_ = function() {
177     this.email_ = null;
178     this.gaiaId_ = null;
179     this.password_ = null;
180     this.oauth_code_ = null;
181     this.chooseWhatToSync_ = false;
182     this.skipForNow_ = false;
183     this.sessionIndex_ = null;
184     this.trusted_ = true;
185     this.authFlow = AuthFlow.DEFAULT;
186     this.samlHandler_.reset();
187   };
189   /**
190    * Loads the authenticator component with the given parameters.
191    * @param {AuthMode} authMode Authorization mode.
192    * @param {Object} data Parameters for the authorization flow.
193    */
194   Authenticator.prototype.load = function(authMode, data) {
195     this.clearCredentials_();
196     this.loaded_ = false;
197     this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN;
198     this.continueUrl_ = data.continueUrl || CONTINUE_URL;
199     this.continueUrlWithoutParams_ =
200         this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) ||
201         this.continueUrl_;
202     this.isConstrainedWindow_ = data.constrained == '1';
203     this.isNewGaiaFlowChromeOS = data.isNewGaiaFlowChromeOS;
204     this.useEafe_ = data.useEafe || false;
205     this.clientId_ = data.clientId;
207     this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
208     this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
209     // Don't block insecure content for desktop flow because it lands on
210     // http. Otherwise, block insecure content as long as gaia is https.
211     this.samlHandler_.blockInsecureContent = authMode != AuthMode.DESKTOP &&
212         this.idpOrigin_.indexOf('https://') == 0;
213     this.needPassword = !('needPassword' in data) || data.needPassword;
215     if (this.isNewGaiaFlowChromeOS) {
216       this.webview_.contextMenus.onShow.addListener(function(e) {
217         e.preventDefault();
218       });
219       if (!this.onBeforeSetHeadersSet_) {
220         this.onBeforeSetHeadersSet_ = true;
221         var filterPrefix = this.idpOrigin_ + EMBEDDED_SETUP_CHROMEOS_ENDPOINT;
222         this.webview_.request.onBeforeSendHeaders.addListener(
223             this.onBeforeSendHeaders_.bind(this),
224             {urls: [filterPrefix + '?*', filterPrefix + '/*']},
225             ['requestHeaders', 'blocking']);
226       }
227     }
229     this.webview_.src = this.reloadUrl_;
230   };
232   /**
233    * Reloads the authenticator component.
234    */
235   Authenticator.prototype.reload = function() {
236     this.clearCredentials_();
237     this.loaded_ = false;
238     this.webview_.src = this.reloadUrl_;
239   };
241   Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
242     var path = data.gaiaPath;
243     if (!path && this.isNewGaiaFlowChromeOS)
244       path = EMBEDDED_SETUP_CHROMEOS_ENDPOINT;
245     if (!path)
246       path = IDP_PATH;
247     var url = this.idpOrigin_ + path;
249     if (this.isNewGaiaFlowChromeOS) {
250       if (data.chromeType)
251         url = appendParam(url, 'chrometype', data.chromeType);
252       if (data.clientId)
253         url = appendParam(url, 'client_id', data.clientId);
254       if (data.enterpriseDomain)
255         url = appendParam(url, 'manageddomain', data.enterpriseDomain);
256       if (data.clientVersion)
257         url = appendParam(url, 'client_version', data.clientVersion);
258       if (data.platformVersion)
259         url = appendParam(url, 'platform_version', data.platformVersion);
260       if (data.releaseChannel)
261         url = appendParam(url, 'release_channel', data.releaseChannel);
262       if (data.endpointGen)
263         url = appendParam(url, 'endpoint_gen', data.endpointGen);
264       this.deviceId_ = data.deviceId;
265       this.sessionIsEphemeral_ = data.sessionIsEphemeral;
266     } else {
267       url = appendParam(url, 'continue', this.continueUrl_);
268       url = appendParam(url, 'service', data.service || SERVICE_ID);
269     }
270     if (data.hl)
271       url = appendParam(url, 'hl', data.hl);
272     if (data.gaiaId)
273       url = appendParam(url, 'user_id', data.gaiaId);
274     if (data.email)
275       url = appendParam(url, 'Email', data.email);
276     if (this.isConstrainedWindow_)
277       url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
278     if (data.flow)
279       url = appendParam(url, 'flow', data.flow);
280     if (data.emailDomain)
281       url = appendParam(url, 'emaildomain', data.emailDomain);
282     return url;
283   };
285   /**
286    * Invoked when a main frame request in the webview has completed.
287    * @private
288    */
289   Authenticator.prototype.onRequestCompleted_ = function(details) {
290     var currentUrl = details.url;
292     if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
293       if (currentUrl.indexOf('ntp=1') >= 0)
294         this.skipForNow_ = true;
296       this.maybeCompleteAuth_();
297       return;
298     }
300     if (currentUrl.indexOf('https') != 0)
301       this.trusted_ = false;
303     if (this.isConstrainedWindow_) {
304       var isEmbeddedPage = false;
305       if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
306         var headers = details.responseHeaders;
307         for (var i = 0; headers && i < headers.length; ++i) {
308           if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) {
309             isEmbeddedPage = true;
310             break;
311           }
312         }
313       }
314       if (!isEmbeddedPage) {
315         this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
316         return;
317       }
318     }
320     this.updateHistoryState_(currentUrl);
321   };
323   /**
324     * Manually updates the history. Invoked upon completion of a webview
325     * navigation.
326     * @param {string} url Request URL.
327     * @private
328     */
329   Authenticator.prototype.updateHistoryState_ = function(url) {
330     if (history.state && history.state.url != url)
331       history.pushState({url: url}, '');
332     else
333       history.replaceState({url: url}, '');
334   };
336   /**
337    * Invoked when the sign-in page takes focus.
338    * @param {object} e The focus event being triggered.
339    * @private
340    */
341   Authenticator.prototype.onFocus_ = function(e) {
342     this.webview_.focus();
343   };
345   /**
346    * Invoked when the history state is changed.
347    * @param {object} e The popstate event being triggered.
348    * @private
349    */
350   Authenticator.prototype.onPopState_ = function(e) {
351     var state = e.state;
352     if (state && state.url)
353       this.webview_.src = state.url;
354   };
356   /**
357    * Invoked when headers are received in the main frame of the webview. It
358    * 1) reads the authenticated user info from a signin header,
359    * 2) signals the start of a saml flow upon receiving a saml header.
360    * @return {!Object} Modified request headers.
361    * @private
362    */
363   Authenticator.prototype.onHeadersReceived_ = function(details) {
364     var currentUrl = details.url;
365     if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0)
366       return;
368     var headers = details.responseHeaders;
369     for (var i = 0; headers && i < headers.length; ++i) {
370       var header = headers[i];
371       var headerName = header.name.toLowerCase();
372       if (headerName == SIGN_IN_HEADER) {
373         var headerValues = header.value.toLowerCase().split(',');
374         var signinDetails = {};
375         headerValues.forEach(function(e) {
376           var pair = e.split('=');
377           signinDetails[pair[0].trim()] = pair[1].trim();
378         });
379         // Removes "" around.
380         this.email_ = signinDetails['email'].slice(1, -1);
381         this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
382         this.sessionIndex_ = signinDetails['sessionindex'];
383       } else if (headerName == LOCATION_HEADER) {
384         // If the "choose what to sync" checkbox was clicked, then the continue
385         // URL will contain a source=3 field.
386         var location = decodeURIComponent(header.value);
387         this.chooseWhatToSync_ = !!location.match(/(\?|&)source=3($|&)/);
388       } else if (
389           this.isNewGaiaFlowChromeOS && headerName == SET_COOKIE_HEADER) {
390         var headerValue = header.value;
391         if (headerValue.indexOf(OAUTH_CODE_COOKIE + '=', 0) == 0) {
392           this.oauth_code_ =
393               headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0];
394         }
395       }
396     }
397   };
399   /**
400    * Handler for webView.request.onBeforeSendHeaders .
401    * @return {!Object} Modified request headers.
402    * @private
403    */
404   Authenticator.prototype.onBeforeSendHeaders_ = function(details) {
405     // deviceId_ is empty when we do not need to send it. For example,
406     // in case of device enrollment.
407     if (this.isNewGaiaFlowChromeOS && this.deviceId_) {
408       var headers = details.requestHeaders;
409       var found = false;
410       var deviceId = this.getGAIADeviceId_();
412       for (var i = 0, l = headers.length; i < l; ++i) {
413         if (headers[i].name == X_DEVICE_ID_HEADER) {
414           headers[i].value = deviceId;
415           found = true;
416           break;
417         }
418       }
419       if (!found) {
420         details.requestHeaders.push(
421             {name: X_DEVICE_ID_HEADER, value: deviceId});
422       }
423     }
424     return {
425       requestHeaders: details.requestHeaders
426     };
427   };
429   /**
430    * Returns true if given HTML5 message is received from the webview element.
431    * @param {object} e Payload of the received HTML5 message.
432    */
433   Authenticator.prototype.isGaiaMessage = function(e) {
434     if (!this.isWebviewEvent_(e))
435       return false;
437     // The event origin does not have a trailing slash.
438     if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) {
439       return false;
440     }
442     // EAFE passes back auth code via message.
443     if (this.useEafe_ &&
444         typeof e.data == 'object' &&
445         e.data.hasOwnProperty('authorizationCode')) {
446       assert(!this.oauth_code_);
447       this.oauth_code_ = e.data.authorizationCode;
448       this.dispatchEvent(
449           new CustomEvent('authCompleted',
450                           {
451                             detail: {
452                               authCodeOnly: true,
453                               authCode: this.oauth_code_
454                             }
455                           }));
456       return;
457     }
459     // Gaia messages must be an object with 'method' property.
460     if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) {
461       return false;
462     }
463     return true;
464   };
466   /**
467    * Invoked when an HTML5 message is received from the webview element.
468    * @param {object} e Payload of the received HTML5 message.
469    * @private
470    */
471   Authenticator.prototype.onMessageFromWebview_ = function(e) {
472     if (!this.isGaiaMessage(e))
473       return;
475     var msg = e.data;
476     if (msg.method == 'attemptLogin') {
477       this.email_ = msg.email;
478       this.password_ = msg.password;
479       this.chooseWhatToSync_ = msg.chooseWhatToSync;
480       // We need to dispatch only first event, before user enters password.
481       if (!msg.password) {
482         this.dispatchEvent(
483             new CustomEvent('attemptLogin', {detail: msg.email}));
484       }
485     } else if (msg.method == 'dialogShown') {
486       this.dispatchEvent(new Event('dialogShown'));
487     } else if (msg.method == 'dialogHidden') {
488       this.dispatchEvent(new Event('dialogHidden'));
489     } else if (msg.method == 'backButton') {
490       this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show}));
491     } else if (msg.method == 'showView') {
492       this.dispatchEvent(new Event('showView'));
493     } else {
494       console.warn('Unrecognized message from GAIA: ' + msg.method);
495     }
496   };
498   /**
499    * Invoked by the hosting page to verify the Saml password.
500    */
501   Authenticator.prototype.verifyConfirmedPassword = function(password) {
502     if (!this.samlHandler_.verifyConfirmedPassword(password)) {
503       // Invoke confirm password callback asynchronously because the
504       // verification was based on messages and caller (GaiaSigninScreen)
505       // does not expect it to be called immediately.
506       // TODO(xiyuan): Change to synchronous call when iframe based code
507       // is removed.
508       var invokeConfirmPassword = (function() {
509         this.confirmPasswordCallback(this.email_,
510                                      this.samlHandler_.scrapedPasswordCount);
511       }).bind(this);
512       window.setTimeout(invokeConfirmPassword, 0);
513       return;
514     }
516     this.password_ = password;
517     this.onAuthCompleted_();
518   };
520   /**
521    * Check Saml flow and start password confirmation flow if needed. Otherwise,
522    * continue with auto completion.
523    * @private
524    */
525   Authenticator.prototype.maybeCompleteAuth_ = function() {
526     var missingGaiaInfo = !this.email_ || !this.gaiaId_ || !this.sessionIndex_;
527     if (missingGaiaInfo && !this.skipForNow_) {
528       if (this.missingGaiaInfoCallback)
529         this.missingGaiaInfoCallback();
531       this.webview_.src = this.initialFrameUrl_;
532       return;
533     }
535     if (this.authFlow != AuthFlow.SAML) {
536       this.onAuthCompleted_();
537       return;
538     }
540     if (this.samlHandler_.samlApiUsed) {
541       if (this.samlApiUsedCallback) {
542         this.samlApiUsedCallback();
543       }
544       this.password_ = this.samlHandler_.apiPasswordBytes;
545     } else if (this.samlHandler_.scrapedPasswordCount == 0) {
546       if (this.noPasswordCallback) {
547         this.noPasswordCallback(this.email_);
548       } else {
549         console.error('Authenticator: No password scraped for SAML.');
550       }
551       return;
552     } else if (this.needPassword) {
553       if (this.confirmPasswordCallback) {
554         // Confirm scraped password. The flow follows in
555         // verifyConfirmedPassword.
556         this.confirmPasswordCallback(this.email_,
557                                      this.samlHandler_.scrapedPasswordCount);
558         return;
559       }
560     }
562     this.onAuthCompleted_();
563   };
565   /**
566    * Invoked to process authentication completion.
567    * @private
568    */
569   Authenticator.prototype.onAuthCompleted_ = function() {
570     assert(this.skipForNow_ ||
571            (this.email_ && this.gaiaId_ && this.sessionIndex_));
572     this.dispatchEvent(
573         new CustomEvent('authCompleted',
574                         // TODO(rsorokin): get rid of the stub values.
575                         {
576                           detail: {
577                             email: this.email_ || '',
578                             gaiaId: this.gaiaId_ || '',
579                             password: this.password_ || '',
580                             authCode: this.oauth_code_,
581                             usingSAML: this.authFlow == AuthFlow.SAML,
582                             chooseWhatToSync: this.chooseWhatToSync_,
583                             skipForNow: this.skipForNow_,
584                             sessionIndex: this.sessionIndex_ || '',
585                             trusted: this.trusted_,
586                             deviceId: this.deviceId_ || ''
587                           }
588                         }));
589     this.clearCredentials_();
590   };
592   /**
593    * Invoked when |samlHandler_| fires 'insecureContentBlocked' event.
594    * @private
595    */
596   Authenticator.prototype.onInsecureContentBlocked_ = function(e) {
597     if (this.insecureContentBlockedCallback) {
598       this.insecureContentBlockedCallback(e.detail.url);
599     } else {
600       console.error('Authenticator: Insecure content blocked.');
601     }
602   };
604   /**
605    * Invoked when |samlHandler_| fires 'authPageLoaded' event.
606    * @private
607    */
608   Authenticator.prototype.onAuthPageLoaded_ = function(e) {
609     if (!e.detail.isSAMLPage)
610       return;
612     this.authDomain = this.samlHandler_.authDomain;
613     this.authFlow = AuthFlow.SAML;
614   };
616   /**
617    * Invoked when a link is dropped on the webview.
618    * @private
619    */
620   Authenticator.prototype.onDropLink_ = function(e) {
621     this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
622   };
624   /**
625    * Invoked when the webview attempts to open a new window.
626    * @private
627    */
628   Authenticator.prototype.onNewWindow_ = function(e) {
629     this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
630   };
632   /**
633    * Invoked when a new document is loaded.
634    * @private
635    */
636   Authenticator.prototype.onContentLoad_ = function(e) {
637     if (this.isConstrainedWindow_) {
638       // Signin content in constrained windows should not zoom. Isolate the
639       // webview from the zooming of other webviews using the 'per-view' zoom
640       // mode, and then set it to 100% zoom.
641       this.webview_.setZoomMode('per-view');
642       this.webview_.setZoom(1);
643     }
645     // Posts a message to IdP pages to initiate communication.
646     var currentUrl = this.webview_.src;
647     if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
648       var msg = {
649         'method': 'handshake',
650         'deviceId': this.getGAIADeviceId_(),
651       };
653       this.webview_.contentWindow.postMessage(msg, currentUrl);
654     }
655   };
657   /**
658    * Invoked when the webview fails loading a page.
659    * @private
660    */
661   Authenticator.prototype.onLoadAbort_ = function(e) {
662     this.dispatchEvent(new CustomEvent('loadAbort',
663         {detail: {error: e.reason,
664                   src: this.webview_.src}}));
665   };
667   /**
668    * Invoked when the webview finishes loading a page.
669    * @private
670    */
671   Authenticator.prototype.onLoadStop_ = function(e) {
672     if (!this.loaded_) {
673       this.loaded_ = true;
674       this.dispatchEvent(new Event('ready'));
675       // Focus webview after dispatching event when webview is already visible.
676       this.webview_.focus();
677     }
679     // Sends client id to EAFE on every loadstop after a small timeout. This is
680     // needed because EAFE sits behind SSO and initialize asynchrounouly
681     // and we don't know for sure when it is loaded and ready to listen
682     // for message. The postMessage is guarded by EAFE's origin.
683     if (this.useEafe_) {
684       // An arbitrary small timeout for delivering the initial message.
685       var EAFE_INITIAL_MESSAGE_DELAY_IN_MS = 500;
686       window.setTimeout((function() {
687         var msg = {
688           'clientId': this.clientId_
689         };
690         this.webview_.contentWindow.postMessage(msg, this.idpOrigin_);
691       }).bind(this), EAFE_INITIAL_MESSAGE_DELAY_IN_MS);
692     }
693   };
695   /**
696    * Invoked when the webview navigates withing the current document.
697    * @private
698    */
699   Authenticator.prototype.onLoadCommit_ = function(e) {
700     if (this.oauth_code_) {
701       this.skipForNow_ = true;
702       this.maybeCompleteAuth_();
703     }
704   };
706   /**
707    * Returns |true| if event |e| was sent from the hosted webview.
708    * @private
709    */
710   Authenticator.prototype.isWebviewEvent_ = function(e) {
711     // Note: <webview> prints error message to console if |contentWindow| is not
712     // defined.
713     // TODO(dzhioev): remove the message. http://crbug.com/469522
714     var webviewWindow = this.webview_.contentWindow;
715     return !!webviewWindow && webviewWindow === e.source;
716   };
718   /**
719    * Format deviceId for GAIA .
720    * @return {string} deviceId.
721    * @private
722    */
723   Authenticator.prototype.getGAIADeviceId_ = function() {
724     // deviceId_ is empty when we do not need to send it. For example,
725     // in case of device enrollment.
726     if (!(this.isNewGaiaFlowChromeOS && this.deviceId_))
727       return;
729     if (this.sessionIsEphemeral_)
730       return EPHEMERAL_DEVICE_ID_PREFIX + this.deviceId_;
731     else
732       return this.deviceId_;
733   };
735   /**
736    * Informs Gaia of new deviceId to be used.
737    */
738   Authenticator.prototype.updateDeviceId = function(deviceId) {
739     this.deviceId_ = deviceId;
740     var msg = {
741       'method': 'updateDeviceId',
742       'deviceId': this.getGAIADeviceId_(),
743     };
745     var currentUrl = this.webview_.src;
746     this.webview_.contentWindow.postMessage(msg, currentUrl);
747   };
749   /**
750    * The current auth flow of the hosted auth page.
751    * @type {AuthFlow}
752    */
753   cr.defineProperty(Authenticator, 'authFlow');
755   /**
756    * The domain name of the current auth page.
757    * @type {string}
758    */
759   cr.defineProperty(Authenticator, 'authDomain');
761   Authenticator.AuthFlow = AuthFlow;
762   Authenticator.AuthMode = AuthMode;
763   Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
765   return {
766     // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
767     // iframe-based flow is deprecated.
768     GaiaAuthHost: Authenticator,
769     Authenticator: Authenticator
770   };