Credential passing is the only way for passwords now.
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth_host / authenticator.js
blob3b9f0038430c521ccdfe24aee1b17ed563ec4357
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 COOKIE_HEADER = 'cookie';
29   var SET_COOKIE_HEADER = 'set-cookie';
30   var OAUTH_CODE_COOKIE = 'oauth_code';
31   var GAPS_COOKIE = 'GAPS';
32   var SERVICE_ID = 'chromeoslogin';
33   var EMBEDDED_SETUP_CHROMEOS_ENDPOINT = 'embedded/setup/chromeos';
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     'clientVersion',       // Version of the Chrome build.
89     'platformVersion',     // Version of the OS build.
90     'releaseChannel',      // Installation channel.
91     'endpointGen',         // Current endpoint generation.
92     'gapsCookie',          // GAPS cookie
93   ];
95   /**
96    * Initializes the authenticator component.
97    * @param {webview|string} webview The webview element or its ID to host IdP
98    *     web pages.
99    * @constructor
100    */
101   function Authenticator(webview) {
102     this.webview_ = typeof webview == 'string' ? $(webview) : webview;
103     assert(this.webview_);
105     this.email_ = null;
106     this.password_ = null;
107     this.gaiaId_ = null,
108     this.sessionIndex_ = null;
109     this.chooseWhatToSync_ = false;
110     this.skipForNow_ = false;
111     this.authFlow = AuthFlow.DEFAULT;
112     this.authDomain = '';
113     this.idpOrigin_ = null;
114     this.continueUrl_ = null;
115     this.continueUrlWithoutParams_ = null;
116     this.initialFrameUrl_ = null;
117     this.reloadUrl_ = null;
118     this.trusted_ = true;
119     this.oauthCode_ = null;
120     this.gapsCookie_ = null;
121     this.gapsCookieSent_ = false;
122     this.newGapsCookie_ = null;
124     this.useEafe_ = false;
125     this.clientId_ = null;
127     this.samlHandler_ = new cr.login.SamlHandler(this.webview_);
128     this.confirmPasswordCallback = null;
129     this.noPasswordCallback = null;
130     this.insecureContentBlockedCallback = null;
131     this.samlApiUsedCallback = null;
132     this.missingGaiaInfoCallback = null;
133     this.needPassword = true;
134     this.samlHandler_.addEventListener(
135         'insecureContentBlocked',
136         this.onInsecureContentBlocked_.bind(this));
137     this.samlHandler_.addEventListener(
138         'authPageLoaded',
139         this.onAuthPageLoaded_.bind(this));
141     this.webview_.addEventListener('droplink', this.onDropLink_.bind(this));
142     this.webview_.addEventListener(
143         'newwindow', this.onNewWindow_.bind(this));
144     this.webview_.addEventListener(
145         'contentload', this.onContentLoad_.bind(this));
146     this.webview_.addEventListener(
147         'loadabort', this.onLoadAbort_.bind(this));
148     this.webview_.addEventListener(
149         'loadstop', this.onLoadStop_.bind(this));
150     this.webview_.addEventListener(
151         'loadcommit', this.onLoadCommit_.bind(this));
152     this.webview_.request.onCompleted.addListener(
153         this.onRequestCompleted_.bind(this),
154         {urls: ['<all_urls>'], types: ['main_frame']},
155         ['responseHeaders']);
156     this.webview_.request.onHeadersReceived.addListener(
157         this.onHeadersReceived_.bind(this),
158         {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
159         ['responseHeaders']);
160     window.addEventListener(
161         'message', this.onMessageFromWebview_.bind(this), false);
162     window.addEventListener(
163         'focus', this.onFocus_.bind(this), false);
164     window.addEventListener(
165         'popstate', this.onPopState_.bind(this), false);
166   }
168   Authenticator.prototype = Object.create(cr.EventTarget.prototype);
170   /**
171    * Reinitializes authentication parameters so that a failed login attempt
172    * would not result in an infinite loop.
173    */
174   Authenticator.prototype.clearCredentials_ = function() {
175     this.email_ = null;
176     this.gaiaId_ = null;
177     this.password_ = null;
178     this.oauthCode_ = null;
179     this.gapsCookie_ = null;
180     this.gapsCookieSent_ = false;
181     this.newGapsCookie_ = null;
182     this.chooseWhatToSync_ = false;
183     this.skipForNow_ = false;
184     this.sessionIndex_ = null;
185     this.trusted_ = true;
186     this.authFlow = AuthFlow.DEFAULT;
187     this.samlHandler_.reset();
188   };
190   /**
191    * Loads the authenticator component with the given parameters.
192    * @param {AuthMode} authMode Authorization mode.
193    * @param {Object} data Parameters for the authorization flow.
194    */
195   Authenticator.prototype.load = function(authMode, data) {
196     this.authMode = authMode;
197     this.clearCredentials_();
198     // gaiaUrl parameter is used for testing. Once defined, it is never changed.
199     this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN;
200     this.continueUrl_ = data.continueUrl || CONTINUE_URL;
201     this.continueUrlWithoutParams_ =
202         this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) ||
203         this.continueUrl_;
204     this.isConstrainedWindow_ = data.constrained == '1';
205     this.isNewGaiaFlowChromeOS = data.isNewGaiaFlowChromeOS;
206     this.useEafe_ = data.useEafe || false;
207     this.clientId_ = data.clientId;
208     this.gapsCookie_ = data.gapsCookie;
209     this.gapsCookieSent_ = false;
210     this.newGapsCookie_ = null;
212     this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
213     this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
214     // Don't block insecure content for desktop flow because it lands on
215     // http. Otherwise, block insecure content as long as gaia is https.
216     this.samlHandler_.blockInsecureContent = authMode != AuthMode.DESKTOP &&
217         this.idpOrigin_.indexOf('https://') == 0;
218     this.needPassword = !('needPassword' in data) || data.needPassword;
220     if (this.isNewGaiaFlowChromeOS) {
221       this.webview_.contextMenus.onShow.addListener(function(e) {
222         e.preventDefault();
223       });
225       if (!this.onBeforeSetHeadersSet_) {
226         this.onBeforeSetHeadersSet_ = true;
227         var filterPrefix = this.idpOrigin_ + EMBEDDED_SETUP_CHROMEOS_ENDPOINT;
228         // This depends on gaiaUrl parameter, that is why it is here.
229         this.webview_.request.onBeforeSendHeaders.addListener(
230             this.onBeforeSendHeaders_.bind(this),
231             {urls: [filterPrefix + '?*', filterPrefix + '/*']},
232             ['requestHeaders', 'blocking']);
233       }
234     }
236     this.webview_.src = this.reloadUrl_;
237   };
239   /**
240    * Reloads the authenticator component.
241    */
242   Authenticator.prototype.reload = function() {
243     this.clearCredentials_();
244     this.webview_.src = this.reloadUrl_;
245   };
247   Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
248     var path = data.gaiaPath;
249     if (!path && this.isNewGaiaFlowChromeOS)
250       path = EMBEDDED_SETUP_CHROMEOS_ENDPOINT;
251     if (!path)
252       path = IDP_PATH;
253     var url = this.idpOrigin_ + path;
255     if (this.isNewGaiaFlowChromeOS) {
256       if (data.chromeType)
257         url = appendParam(url, 'chrometype', data.chromeType);
258       if (data.clientId)
259         url = appendParam(url, 'client_id', data.clientId);
260       if (data.enterpriseDomain)
261         url = appendParam(url, 'manageddomain', data.enterpriseDomain);
262       if (data.clientVersion)
263         url = appendParam(url, 'client_version', data.clientVersion);
264       if (data.platformVersion)
265         url = appendParam(url, 'platform_version', data.platformVersion);
266       if (data.releaseChannel)
267         url = appendParam(url, 'release_channel', data.releaseChannel);
268       if (data.endpointGen)
269         url = appendParam(url, 'endpoint_gen', data.endpointGen);
270     } else {
271       url = appendParam(url, 'continue', this.continueUrl_);
272       url = appendParam(url, 'service', data.service || SERVICE_ID);
273     }
274     if (data.hl)
275       url = appendParam(url, 'hl', data.hl);
276     if (data.gaiaId)
277       url = appendParam(url, 'user_id', data.gaiaId);
278     if (data.email)
279       url = appendParam(url, 'Email', data.email);
280     if (this.isConstrainedWindow_)
281       url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
282     if (data.flow)
283       url = appendParam(url, 'flow', data.flow);
284     if (data.emailDomain)
285       url = appendParam(url, 'emaildomain', data.emailDomain);
286     return url;
287   };
289   /**
290    * Invoked when a main frame request in the webview has completed.
291    * @private
292    */
293   Authenticator.prototype.onRequestCompleted_ = function(details) {
294     var currentUrl = details.url;
296     if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
297       if (currentUrl.indexOf('ntp=1') >= 0)
298         this.skipForNow_ = true;
300       this.maybeCompleteAuth_();
301       return;
302     }
304     if (currentUrl.indexOf('https') != 0)
305       this.trusted_ = false;
307     if (this.isConstrainedWindow_) {
308       var isEmbeddedPage = false;
309       if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
310         var headers = details.responseHeaders;
311         for (var i = 0; headers && i < headers.length; ++i) {
312           if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) {
313             isEmbeddedPage = true;
314             break;
315           }
316         }
317       }
318       if (!isEmbeddedPage) {
319         this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
320         return;
321       }
322     }
324     this.updateHistoryState_(currentUrl);
325   };
327   /**
328     * Manually updates the history. Invoked upon completion of a webview
329     * navigation.
330     * @param {string} url Request URL.
331     * @private
332     */
333   Authenticator.prototype.updateHistoryState_ = function(url) {
334     if (history.state && history.state.url != url)
335       history.pushState({url: url}, '');
336     else
337       history.replaceState({url: url}, '');
338   };
340   /**
341    * Invoked when the sign-in page takes focus.
342    * @param {object} e The focus event being triggered.
343    * @private
344    */
345   Authenticator.prototype.onFocus_ = function(e) {
346     if (this.authMode == AuthMode.DESKTOP)
347       this.webview_.focus();
348   };
350   /**
351    * Invoked when the history state is changed.
352    * @param {object} e The popstate event being triggered.
353    * @private
354    */
355   Authenticator.prototype.onPopState_ = function(e) {
356     var state = e.state;
357     if (state && state.url)
358       this.webview_.src = state.url;
359   };
361   /**
362    * Invoked when headers are received in the main frame of the webview. It
363    * 1) reads the authenticated user info from a signin header,
364    * 2) signals the start of a saml flow upon receiving a saml header.
365    * @return {!Object} Modified request headers.
366    * @private
367    */
368   Authenticator.prototype.onHeadersReceived_ = function(details) {
369     var currentUrl = details.url;
370     if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0)
371       return;
373     var headers = details.responseHeaders;
374     for (var i = 0; headers && i < headers.length; ++i) {
375       var header = headers[i];
376       var headerName = header.name.toLowerCase();
377       if (headerName == SIGN_IN_HEADER) {
378         var headerValues = header.value.toLowerCase().split(',');
379         var signinDetails = {};
380         headerValues.forEach(function(e) {
381           var pair = e.split('=');
382           signinDetails[pair[0].trim()] = pair[1].trim();
383         });
384         // Removes "" around.
385         this.email_ = signinDetails['email'].slice(1, -1);
386         this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
387         this.sessionIndex_ = signinDetails['sessionindex'];
388       } else if (headerName == LOCATION_HEADER) {
389         // If the "choose what to sync" checkbox was clicked, then the continue
390         // URL will contain a source=3 field.
391         var location = decodeURIComponent(header.value);
392         this.chooseWhatToSync_ = !!location.match(/(\?|&)source=3($|&)/);
393       } else if (
394           this.isNewGaiaFlowChromeOS && headerName == SET_COOKIE_HEADER) {
395         var headerValue = header.value;
396         if (headerValue.indexOf(OAUTH_CODE_COOKIE + '=', 0) == 0) {
397           this.oauthCode_ =
398               headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0];
399         }
400         if (headerValue.indexOf(GAPS_COOKIE + '=', 0) == 0) {
401           this.newGapsCookie_ =
402               headerValue.substring(GAPS_COOKIE.length + 1).split(';')[0];
403         }
404       }
405     }
406   };
408   /**
409    * This method replaces cookie value in cookie header.
410    * @param@ {string} header_value Original string value of Cookie header.
411    * @param@ {string} cookie_name Name of cookie to be replaced.
412    * @param@ {string} cookie_value New cookie value.
413    * @return {string} New Cookie header value.
414    * @private
415    */
416   Authenticator.prototype.updateCookieValue_ = function(
417       header_value, cookie_name, cookie_value) {
418     var cookies = header_value.split(/\s*;\s*/);
419     var found = false;
420     for (var i = 0; i < cookies.length; ++i) {
421       if (cookies[i].indexOf(cookie_name + '=', 0) == 0) {
422         found = true;
423         cookies[i] = cookie_name + '=' + cookie_value;
424         break;
425       }
426     }
427     if (!found) {
428       cookies.push(cookie_name + '=' + cookie_value);
429     }
430     return cookies.join('; ');
431   };
433   /**
434    * Handler for webView.request.onBeforeSendHeaders .
435    * @return {!Object} Modified request headers.
436    * @private
437    */
438   Authenticator.prototype.onBeforeSendHeaders_ = function(details) {
439     // We should re-send cookie if first request was unsuccessful (i.e. no new
440     // GAPS cookie was received).
441     if (this.isNewGaiaFlowChromeOS && this.gapsCookie_ &&
442         (!this.gapsCookieSent_ || !this.newGapsCookie_)) {
443       var headers = details.requestHeaders;
444       var found = false;
445       var gapsCookie = this.gapsCookie_;
447       for (var i = 0, l = headers.length; i < l; ++i) {
448         if (headers[i].name == COOKIE_HEADER) {
449           headers[i].value = this.updateCookieValue_(headers[i].value,
450                                                      GAPS_COOKIE, gapsCookie);
451           found = true;
452           break;
453         }
454       }
455       if (!found) {
456         details.requestHeaders.push(
457             {name: COOKIE_HEADER, value: GAPS_COOKIE + '=' + gapsCookie});
458       }
459       this.gapsCookieSent_ = true;
460     }
461     return {
462       requestHeaders: details.requestHeaders
463     };
464   };
466   /**
467    * Returns true if given HTML5 message is received from the webview element.
468    * @param {object} e Payload of the received HTML5 message.
469    */
470   Authenticator.prototype.isGaiaMessage = function(e) {
471     if (!this.isWebviewEvent_(e))
472       return false;
474     // The event origin does not have a trailing slash.
475     if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) {
476       return false;
477     }
479     // EAFE passes back auth code via message.
480     if (this.useEafe_ &&
481         typeof e.data == 'object' &&
482         e.data.hasOwnProperty('authorizationCode')) {
483       assert(!this.oauthCode_);
484       this.oauthCode_ = e.data.authorizationCode;
485       this.dispatchEvent(
486           new CustomEvent('authCompleted',
487                           {
488                             detail: {
489                               authCodeOnly: true,
490                               authCode: this.oauthCode_
491                             }
492                           }));
493       return;
494     }
496     // Gaia messages must be an object with 'method' property.
497     if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) {
498       return false;
499     }
500     return true;
501   };
503   /**
504    * Invoked when an HTML5 message is received from the webview element.
505    * @param {object} e Payload of the received HTML5 message.
506    * @private
507    */
508   Authenticator.prototype.onMessageFromWebview_ = function(e) {
509     if (!this.isGaiaMessage(e))
510       return;
512     var msg = e.data;
513     if (msg.method == 'attemptLogin') {
514       this.email_ = msg.email;
515       this.chooseWhatToSync_ = msg.chooseWhatToSync;
516       // We need to dispatch only first event, before user enters password.
517       this.dispatchEvent(
518           new CustomEvent('attemptLogin', {detail: msg.email}));
519     } else if (msg.method == 'dialogShown') {
520       this.dispatchEvent(new Event('dialogShown'));
521     } else if (msg.method == 'dialogHidden') {
522       this.dispatchEvent(new Event('dialogHidden'));
523     } else if (msg.method == 'backButton') {
524       this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show}));
525     } else if (msg.method == 'showView') {
526       this.dispatchEvent(new Event('showView'));
527     } else if (msg.method == 'identifierEntered') {
528       this.dispatchEvent(new CustomEvent(
529           'identifierEntered',
530           {detail: {accountIdentifier: msg.accountIdentifier}}));
531     } else {
532       console.warn('Unrecognized message from GAIA: ' + msg.method);
533     }
534   };
536   /**
537    * Invoked by the hosting page to verify the Saml password.
538    */
539   Authenticator.prototype.verifyConfirmedPassword = function(password) {
540     if (!this.samlHandler_.verifyConfirmedPassword(password)) {
541       // Invoke confirm password callback asynchronously because the
542       // verification was based on messages and caller (GaiaSigninScreen)
543       // does not expect it to be called immediately.
544       // TODO(xiyuan): Change to synchronous call when iframe based code
545       // is removed.
546       var invokeConfirmPassword = (function() {
547         this.confirmPasswordCallback(this.email_,
548                                      this.samlHandler_.scrapedPasswordCount);
549       }).bind(this);
550       window.setTimeout(invokeConfirmPassword, 0);
551       return;
552     }
554     this.password_ = password;
555     this.onAuthCompleted_();
556   };
558   /**
559    * Check Saml flow and start password confirmation flow if needed. Otherwise,
560    * continue with auto completion.
561    * @private
562    */
563   Authenticator.prototype.maybeCompleteAuth_ = function() {
564     var missingGaiaInfo = !this.email_ || !this.gaiaId_ || !this.sessionIndex_;
565     if (missingGaiaInfo && !this.skipForNow_) {
566       if (this.missingGaiaInfoCallback)
567         this.missingGaiaInfoCallback();
569       this.webview_.src = this.initialFrameUrl_;
570       return;
571     }
573     if (this.samlHandler_.samlApiUsed) {
574       if (this.samlApiUsedCallback) {
575         this.samlApiUsedCallback();
576       }
577       this.password_ = this.samlHandler_.apiPasswordBytes;
578       this.onAuthCompleted_();
579       return;
580     }
582     if (this.samlHandler_.scrapedPasswordCount == 0) {
583       if (this.noPasswordCallback) {
584         this.noPasswordCallback(this.email_);
585         return;
586       }
588       // Fall through to finish the auth flow even if this.needPassword
589       // is true. This is because the flag is used as an intention to get
590       // password when it is available but not a mandatory requirement.
591       console.warn('Authenticator: No password scraped for SAML.');
592     } else if (this.needPassword) {
593       if (this.confirmPasswordCallback) {
594         // Confirm scraped password. The flow follows in
595         // verifyConfirmedPassword.
596         this.confirmPasswordCallback(this.email_,
597                                      this.samlHandler_.scrapedPasswordCount);
598         return;
599       }
600     }
602     this.onAuthCompleted_();
603   };
605   /**
606    * Invoked to process authentication completion.
607    * @private
608    */
609   Authenticator.prototype.onAuthCompleted_ = function() {
610     assert(this.skipForNow_ ||
611            (this.email_ && this.gaiaId_ && this.sessionIndex_));
612     this.dispatchEvent(new CustomEvent(
613         'authCompleted',
614         // TODO(rsorokin): get rid of the stub values.
615         {
616           detail: {
617             email: this.email_ || '',
618             gaiaId: this.gaiaId_ || '',
619             password: this.password_ || '',
620             authCode: this.oauthCode_,
621             usingSAML: this.authFlow == AuthFlow.SAML,
622             chooseWhatToSync: this.chooseWhatToSync_,
623             skipForNow: this.skipForNow_,
624             sessionIndex: this.sessionIndex_ || '',
625             trusted: this.trusted_,
626             gapsCookie: this.newGapsCookie_ || this.gapsCookie_ || '',
627           }
628         }));
629     this.clearCredentials_();
630   };
632   /**
633    * Invoked when |samlHandler_| fires 'insecureContentBlocked' event.
634    * @private
635    */
636   Authenticator.prototype.onInsecureContentBlocked_ = function(e) {
637     if (this.insecureContentBlockedCallback) {
638       this.insecureContentBlockedCallback(e.detail.url);
639     } else {
640       console.error('Authenticator: Insecure content blocked.');
641     }
642   };
644   /**
645    * Invoked when |samlHandler_| fires 'authPageLoaded' event.
646    * @private
647    */
648   Authenticator.prototype.onAuthPageLoaded_ = function(e) {
649     if (!e.detail.isSAMLPage)
650       return;
652     this.authDomain = this.samlHandler_.authDomain;
653     this.authFlow = AuthFlow.SAML;
654   };
656   /**
657    * Invoked when a link is dropped on the webview.
658    * @private
659    */
660   Authenticator.prototype.onDropLink_ = function(e) {
661     this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
662   };
664   /**
665    * Invoked when the webview attempts to open a new window.
666    * @private
667    */
668   Authenticator.prototype.onNewWindow_ = function(e) {
669     this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
670   };
672   /**
673    * Invoked when a new document is loaded.
674    * @private
675    */
676   Authenticator.prototype.onContentLoad_ = function(e) {
677     if (this.isConstrainedWindow_) {
678       // Signin content in constrained windows should not zoom. Isolate the
679       // webview from the zooming of other webviews using the 'per-view' zoom
680       // mode, and then set it to 100% zoom.
681       this.webview_.setZoomMode('per-view');
682       this.webview_.setZoom(1);
683     }
685     // Posts a message to IdP pages to initiate communication.
686     var currentUrl = this.webview_.src;
687     if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
688       var msg = {
689         'method': 'handshake',
690       };
692       this.webview_.contentWindow.postMessage(msg, currentUrl);
694       this.dispatchEvent(new Event('ready'));
695       // Focus webview after dispatching event when webview is already visible.
696       this.webview_.focus();
697     }
698   };
700   /**
701    * Invoked when the webview fails loading a page.
702    * @private
703    */
704   Authenticator.prototype.onLoadAbort_ = function(e) {
705     this.dispatchEvent(new CustomEvent('loadAbort',
706         {detail: {error: e.reason,
707                   src: this.webview_.src}}));
708   };
710   /**
711    * Invoked when the webview finishes loading a page.
712    * @private
713    */
714   Authenticator.prototype.onLoadStop_ = function(e) {
715     // Sends client id to EAFE on every loadstop after a small timeout. This is
716     // needed because EAFE sits behind SSO and initialize asynchrounouly
717     // and we don't know for sure when it is loaded and ready to listen
718     // for message. The postMessage is guarded by EAFE's origin.
719     if (this.useEafe_) {
720       // An arbitrary small timeout for delivering the initial message.
721       var EAFE_INITIAL_MESSAGE_DELAY_IN_MS = 500;
722       window.setTimeout((function() {
723         var msg = {
724           'clientId': this.clientId_
725         };
726         this.webview_.contentWindow.postMessage(msg, this.idpOrigin_);
727       }).bind(this), EAFE_INITIAL_MESSAGE_DELAY_IN_MS);
728     }
729   };
731   /**
732    * Invoked when the webview navigates withing the current document.
733    * @private
734    */
735   Authenticator.prototype.onLoadCommit_ = function(e) {
736     if (this.oauthCode_) {
737       this.skipForNow_ = true;
738       this.maybeCompleteAuth_();
739     }
740   };
742   /**
743    * Returns |true| if event |e| was sent from the hosted webview.
744    * @private
745    */
746   Authenticator.prototype.isWebviewEvent_ = function(e) {
747     // Note: <webview> prints error message to console if |contentWindow| is not
748     // defined.
749     // TODO(dzhioev): remove the message. http://crbug.com/469522
750     var webviewWindow = this.webview_.contentWindow;
751     return !!webviewWindow && webviewWindow === e.source;
752   };
754   /**
755    * The current auth flow of the hosted auth page.
756    * @type {AuthFlow}
757    */
758   cr.defineProperty(Authenticator, 'authFlow');
760   /**
761    * The domain name of the current auth page.
762    * @type {string}
763    */
764   cr.defineProperty(Authenticator, 'authDomain');
766   Authenticator.AuthFlow = AuthFlow;
767   Authenticator.AuthMode = AuthMode;
768   Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
770   return {
771     // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
772     // iframe-based flow is deprecated.
773     GaiaAuthHost: Authenticator,
774     Authenticator: Authenticator
775   };