Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth_host / authenticator.js
blob99a072849c07d38078349c600498342c4a771a69
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.password_ = msg.password;
516       this.chooseWhatToSync_ = msg.chooseWhatToSync;
517       // We need to dispatch only first event, before user enters password.
518       if (!msg.password) {
519         this.dispatchEvent(
520             new CustomEvent('attemptLogin', {detail: msg.email}));
521       }
522     } else if (msg.method == 'dialogShown') {
523       this.dispatchEvent(new Event('dialogShown'));
524     } else if (msg.method == 'dialogHidden') {
525       this.dispatchEvent(new Event('dialogHidden'));
526     } else if (msg.method == 'backButton') {
527       this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show}));
528     } else if (msg.method == 'showView') {
529       this.dispatchEvent(new Event('showView'));
530     } else if (msg.method == 'identifierEntered') {
531       this.dispatchEvent(new CustomEvent(
532           'identifierEntered',
533           {detail: {accountIdentifier: msg.accountIdentifier}}));
534     } else {
535       console.warn('Unrecognized message from GAIA: ' + msg.method);
536     }
537   };
539   /**
540    * Invoked by the hosting page to verify the Saml password.
541    */
542   Authenticator.prototype.verifyConfirmedPassword = function(password) {
543     if (!this.samlHandler_.verifyConfirmedPassword(password)) {
544       // Invoke confirm password callback asynchronously because the
545       // verification was based on messages and caller (GaiaSigninScreen)
546       // does not expect it to be called immediately.
547       // TODO(xiyuan): Change to synchronous call when iframe based code
548       // is removed.
549       var invokeConfirmPassword = (function() {
550         this.confirmPasswordCallback(this.email_,
551                                      this.samlHandler_.scrapedPasswordCount);
552       }).bind(this);
553       window.setTimeout(invokeConfirmPassword, 0);
554       return;
555     }
557     this.password_ = password;
558     this.onAuthCompleted_();
559   };
561   /**
562    * Check Saml flow and start password confirmation flow if needed. Otherwise,
563    * continue with auto completion.
564    * @private
565    */
566   Authenticator.prototype.maybeCompleteAuth_ = function() {
567     var missingGaiaInfo = !this.email_ || !this.gaiaId_ || !this.sessionIndex_;
568     if (missingGaiaInfo && !this.skipForNow_) {
569       if (this.missingGaiaInfoCallback)
570         this.missingGaiaInfoCallback();
572       this.webview_.src = this.initialFrameUrl_;
573       return;
574     }
576     if (this.samlHandler_.samlApiUsed) {
577       if (this.samlApiUsedCallback) {
578         this.samlApiUsedCallback();
579       }
580       this.password_ = this.samlHandler_.apiPasswordBytes;
581       this.onAuthCompleted_();
582       return;
583     }
585     // TODO(achuith): Eliminate this branch when credential passing api is
586     // stable on prod. crbug.com/467778.
587     if (this.authFlow != AuthFlow.SAML) {
588       this.onAuthCompleted_();
589       return;
590     }
592     if (this.samlHandler_.scrapedPasswordCount == 0) {
593       if (this.noPasswordCallback) {
594         this.noPasswordCallback(this.email_);
595         return;
596       }
598       // Fall through to finish the auth flow even if this.needPassword
599       // is true. This is because the flag is used as an intention to get
600       // password when it is available but not a mandatory requirement.
601       console.warn('Authenticator: No password scraped for SAML.');
602     } else if (this.needPassword) {
603       if (this.confirmPasswordCallback) {
604         // Confirm scraped password. The flow follows in
605         // verifyConfirmedPassword.
606         this.confirmPasswordCallback(this.email_,
607                                      this.samlHandler_.scrapedPasswordCount);
608         return;
609       }
610     }
612     this.onAuthCompleted_();
613   };
615   /**
616    * Invoked to process authentication completion.
617    * @private
618    */
619   Authenticator.prototype.onAuthCompleted_ = function() {
620     assert(this.skipForNow_ ||
621            (this.email_ && this.gaiaId_ && this.sessionIndex_));
622     this.dispatchEvent(new CustomEvent(
623         'authCompleted',
624         // TODO(rsorokin): get rid of the stub values.
625         {
626           detail: {
627             email: this.email_ || '',
628             gaiaId: this.gaiaId_ || '',
629             password: this.password_ || '',
630             authCode: this.oauthCode_,
631             usingSAML: this.authFlow == AuthFlow.SAML,
632             chooseWhatToSync: this.chooseWhatToSync_,
633             skipForNow: this.skipForNow_,
634             sessionIndex: this.sessionIndex_ || '',
635             trusted: this.trusted_,
636             gapsCookie: this.newGapsCookie_ || this.gapsCookie_ || '',
637           }
638         }));
639     this.clearCredentials_();
640   };
642   /**
643    * Invoked when |samlHandler_| fires 'insecureContentBlocked' event.
644    * @private
645    */
646   Authenticator.prototype.onInsecureContentBlocked_ = function(e) {
647     if (this.insecureContentBlockedCallback) {
648       this.insecureContentBlockedCallback(e.detail.url);
649     } else {
650       console.error('Authenticator: Insecure content blocked.');
651     }
652   };
654   /**
655    * Invoked when |samlHandler_| fires 'authPageLoaded' event.
656    * @private
657    */
658   Authenticator.prototype.onAuthPageLoaded_ = function(e) {
659     if (!e.detail.isSAMLPage)
660       return;
662     this.authDomain = this.samlHandler_.authDomain;
663     this.authFlow = AuthFlow.SAML;
664   };
666   /**
667    * Invoked when a link is dropped on the webview.
668    * @private
669    */
670   Authenticator.prototype.onDropLink_ = function(e) {
671     this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
672   };
674   /**
675    * Invoked when the webview attempts to open a new window.
676    * @private
677    */
678   Authenticator.prototype.onNewWindow_ = function(e) {
679     this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
680   };
682   /**
683    * Invoked when a new document is loaded.
684    * @private
685    */
686   Authenticator.prototype.onContentLoad_ = function(e) {
687     if (this.isConstrainedWindow_) {
688       // Signin content in constrained windows should not zoom. Isolate the
689       // webview from the zooming of other webviews using the 'per-view' zoom
690       // mode, and then set it to 100% zoom.
691       this.webview_.setZoomMode('per-view');
692       this.webview_.setZoom(1);
693     }
695     // Posts a message to IdP pages to initiate communication.
696     var currentUrl = this.webview_.src;
697     if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
698       var msg = {
699         'method': 'handshake',
700       };
702       this.webview_.contentWindow.postMessage(msg, currentUrl);
704       this.dispatchEvent(new Event('ready'));
705       // Focus webview after dispatching event when webview is already visible.
706       this.webview_.focus();
707     }
708   };
710   /**
711    * Invoked when the webview fails loading a page.
712    * @private
713    */
714   Authenticator.prototype.onLoadAbort_ = function(e) {
715     this.dispatchEvent(new CustomEvent('loadAbort',
716         {detail: {error: e.reason,
717                   src: this.webview_.src}}));
718   };
720   /**
721    * Invoked when the webview finishes loading a page.
722    * @private
723    */
724   Authenticator.prototype.onLoadStop_ = function(e) {
725     // Sends client id to EAFE on every loadstop after a small timeout. This is
726     // needed because EAFE sits behind SSO and initialize asynchrounouly
727     // and we don't know for sure when it is loaded and ready to listen
728     // for message. The postMessage is guarded by EAFE's origin.
729     if (this.useEafe_) {
730       // An arbitrary small timeout for delivering the initial message.
731       var EAFE_INITIAL_MESSAGE_DELAY_IN_MS = 500;
732       window.setTimeout((function() {
733         var msg = {
734           'clientId': this.clientId_
735         };
736         this.webview_.contentWindow.postMessage(msg, this.idpOrigin_);
737       }).bind(this), EAFE_INITIAL_MESSAGE_DELAY_IN_MS);
738     }
739   };
741   /**
742    * Invoked when the webview navigates withing the current document.
743    * @private
744    */
745   Authenticator.prototype.onLoadCommit_ = function(e) {
746     if (this.oauthCode_) {
747       this.skipForNow_ = true;
748       this.maybeCompleteAuth_();
749     }
750   };
752   /**
753    * Returns |true| if event |e| was sent from the hosted webview.
754    * @private
755    */
756   Authenticator.prototype.isWebviewEvent_ = function(e) {
757     // Note: <webview> prints error message to console if |contentWindow| is not
758     // defined.
759     // TODO(dzhioev): remove the message. http://crbug.com/469522
760     var webviewWindow = this.webview_.contentWindow;
761     return !!webviewWindow && webviewWindow === e.source;
762   };
764   /**
765    * The current auth flow of the hosted auth page.
766    * @type {AuthFlow}
767    */
768   cr.defineProperty(Authenticator, 'authFlow');
770   /**
771    * The domain name of the current auth page.
772    * @type {string}
773    */
774   cr.defineProperty(Authenticator, 'authDomain');
776   Authenticator.AuthFlow = AuthFlow;
777   Authenticator.AuthMode = AuthMode;
778   Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
780   return {
781     // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
782     // iframe-based flow is deprecated.
783     GaiaAuthHost: Authenticator,
784     Authenticator: Authenticator
785   };