Show error message if authenticated user e-mail retrieval fails
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / login / screen_gaia_signin.js
blobaa88b576b03f3975b6f16c74ae4533affcadbea4
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  * @fileoverview Oobe signin screen implementation.
7  */
9 <include src="../../gaia_auth_host/gaia_auth_host.js"></include>
11 login.createScreen('GaiaSigninScreen', 'gaia-signin', function() {
12   // Gaia loading time after which error message must be displayed and
13   // lazy portal check should be fired.
14   /** @const */ var GAIA_LOADING_PORTAL_SUSSPECT_TIME_SEC = 7;
16   // Maximum Gaia loading time in seconds.
17   /** @const */ var MAX_GAIA_LOADING_TIME_SEC = 60;
19   /** @const */ var HELP_TOPIC_ENTERPRISE_REPORTING = 2535613;
21   /** @const */ var NET_ERROR_ABORTED = 3;
23   /** @const */ var NET_ERROR_DISALLOWED_URL_SCHEME = 301;
25   return {
26     EXTERNAL_API: [
27       'loadAuthExtension',
28       'updateAuthExtension',
29       'setAuthenticatedUserEmail',
30       'doReload',
31       'onFrameError',
32       'updateCancelButtonState'
33     ],
35     /**
36      * Frame loading error code (0 - no error).
37      * @type {number}
38      * @private
39      */
40     error_: 0,
42     /**
43      * Saved gaia auth host load params.
44      * @type {?string}
45      * @private
46      */
47     gaiaAuthParams_: null,
49     /**
50      * Whether local version of Gaia page is used.
51      * @type {boolean}
52      * @private
53      */
54     isLocal_: false,
56     /**
57      * Email of the user, which is logging in using offline mode.
58      * @type {string}
59      */
60     email: '',
62     /**
63      * Timer id of pending load.
64      * @type {number}
65      * @private
66      */
67     loadingTimer_: undefined,
69     /**
70      * Whether user can cancel Gaia screen.
71      * @type {boolean}
72      * @private
73      */
74     cancelAllowed_: undefined,
76     /**
77      * Whether we should show user pods on the login screen.
78      * @type {boolean}
79      * @private
80      */
81     isShowUsers_: undefined,
83     /**
84      * SAML password confirmation attempt count.
85      * @type {number}
86      */
87     samlPasswordConfirmAttempt_: 0,
89     /** @override */
90     decorate: function() {
91       this.gaiaAuthHost_ = new cr.login.GaiaAuthHost($('signin-frame'));
92       this.gaiaAuthHost_.addEventListener(
93           'ready', this.onAuthReady_.bind(this));
94       this.gaiaAuthHost_.retrieveAuthenticatedUserEmailCallback =
95           this.onRetrieveAuthenticatedUserEmail_.bind(this);
96       this.gaiaAuthHost_.confirmPasswordCallback =
97           this.onAuthConfirmPassword_.bind(this);
98       this.gaiaAuthHost_.noPasswordCallback =
99           this.onAuthNoPassword_.bind(this);
100       this.gaiaAuthHost_.addEventListener('authFlowChange',
101           this.onAuthFlowChange_.bind(this));
103       $('enterprise-info-hint-link').addEventListener('click', function(e) {
104         chrome.send('launchHelpApp', [HELP_TOPIC_ENTERPRISE_REPORTING]);
105         e.preventDefault();
106       });
109       this.updateLocalizedContent();
110     },
112     /**
113      * Header text of the screen.
114      * @type {string}
115      */
116     get header() {
117       return loadTimeData.getString('signinScreenTitle');
118     },
120     /**
121      * Returns true if local version of Gaia is used.
122      * @type {boolean}
123      */
124     get isLocal() {
125       return this.isLocal_;
126     },
128     /**
129      * Sets whether local version of Gaia is used.
130      * @param {boolean} value Whether local version of Gaia is used.
131      */
132     set isLocal(value) {
133       this.isLocal_ = value;
134       chrome.send('updateOfflineLogin', [value]);
135     },
137     /**
138      * Shows/hides loading UI.
139      * @param {boolean} show True to show loading UI.
140      * @private
141      */
142     showLoadingUI_: function(show) {
143       $('gaia-loading').hidden = !show;
144       this.gaiaAuthHost_.frame.hidden = show;
145       $('signin-right').hidden = show;
146       $('enterprise-info-container').hidden = show;
147       $('gaia-signin-divider').hidden = show;
148     },
150     /**
151      * Handler for Gaia loading suspiciously long timeout.
152      * @private
153      */
154     onLoadingSuspiciouslyLong_: function() {
155       if (this != Oobe.getInstance().currentScreen)
156         return;
157       chrome.send('showLoadingTimeoutError');
158       this.loadingTimer_ = window.setTimeout(
159           this.onLoadingTimeOut_.bind(this),
160           (MAX_GAIA_LOADING_TIME_SEC - GAIA_LOADING_PORTAL_SUSSPECT_TIME_SEC) *
161           1000);
162     },
164     /**
165      * Handler for Gaia loading timeout.
166      * @private
167      */
168     onLoadingTimeOut_: function() {
169       this.loadingTimer_ = undefined;
170       chrome.send('showLoadingTimeoutError');
171     },
173     /**
174      * Clears loading timer.
175      * @private
176      */
177     clearLoadingTimer_: function() {
178       if (this.loadingTimer_) {
179         window.clearTimeout(this.loadingTimer_);
180         this.loadingTimer_ = undefined;
181       }
182     },
184     /**
185      * Sets up loading timer.
186      * @private
187      */
188     startLoadingTimer_: function() {
189       this.clearLoadingTimer_();
190       this.loadingTimer_ = window.setTimeout(
191           this.onLoadingSuspiciouslyLong_.bind(this),
192           GAIA_LOADING_PORTAL_SUSSPECT_TIME_SEC * 1000);
193     },
195     /**
196      * Whether Gaia is loading.
197      * @type {boolean}
198      */
199     get loading() {
200       return !$('gaia-loading').hidden;
201     },
202     set loading(loading) {
203       if (loading == this.loading)
204         return;
206       this.showLoadingUI_(loading);
207     },
209     /**
210      * Event handler that is invoked just before the frame is shown.
211      * @param {string} data Screen init payload. Url of auth extension start
212      *                      page.
213      */
214     onBeforeShow: function(data) {
215       chrome.send('loginUIStateChanged', ['gaia-signin', true]);
216       $('login-header-bar').signinUIState = SIGNIN_UI_STATE.GAIA_SIGNIN;
218       // Ensure that GAIA signin (or loading UI) is actually visible.
219       window.webkitRequestAnimationFrame(function() {
220         chrome.send('loginVisible', ['gaia-loading']);
221       });
223       // Announce the name of the screen, if accessibility is on.
224       $('gaia-signin-aria-label').setAttribute(
225           'aria-label', loadTimeData.getString('signinScreenTitle'));
227       // Button header is always visible when sign in is presented.
228       // Header is hidden once GAIA reports on successful sign in.
229       Oobe.getInstance().headerHidden = false;
230     },
232     /**
233      * Event handler that is invoked just before the screen is hidden.
234      */
235     onBeforeHide: function() {
236       chrome.send('loginUIStateChanged', ['gaia-signin', false]);
237       $('login-header-bar').signinUIState = SIGNIN_UI_STATE.HIDDEN;
238     },
240     /**
241      * Loads the authentication extension into the iframe.
242      * @param {Object} data Extension parameters bag.
243      * @private
244      */
245     loadAuthExtension: function(data) {
246       this.isLocal = data.isLocal;
247       this.email = '';
249       // Reset SAML
250       this.classList.toggle('saml', false);
251       this.samlPasswordConfirmAttempt_ = 0;
253       this.updateAuthExtension(data);
255       var params = {};
256       for (var i in cr.login.GaiaAuthHost.SUPPORTED_PARAMS) {
257         var name = cr.login.GaiaAuthHost.SUPPORTED_PARAMS[i];
258         if (data[name])
259           params[name] = data[name];
260       }
262       if (data.localizedStrings)
263         params.localizedStrings = data.localizedStrings;
265       if (data.forceReload ||
266           JSON.stringify(this.gaiaAuthParams_) != JSON.stringify(params)) {
267         this.error_ = 0;
268         this.gaiaAuthHost_.load(data.useOffline ?
269                                     cr.login.GaiaAuthHost.AuthMode.OFFLINE :
270                                     cr.login.GaiaAuthHost.AuthMode.DEFAULT,
271                                 params,
272                                 this.onAuthCompleted_.bind(this));
273         this.gaiaAuthParams_ = params;
275         this.loading = true;
276         this.startLoadingTimer_();
277       } else if (this.loading && this.error_) {
278         // An error has occurred, so trying to reload.
279         this.doReload();
280       }
281     },
283     /**
284      * Updates the authentication extension with new parameters, if needed.
285      * @param {Object} data New extension parameters bag.
286      * @private
287      */
288     updateAuthExtension: function(data) {
289       var reasonLabel = $('gaia-signin-reason');
290       if (data.passwordChanged) {
291         reasonLabel.textContent =
292             loadTimeData.getString('signinScreenPasswordChanged');
293         reasonLabel.hidden = false;
294       } else {
295         reasonLabel.hidden = true;
296       }
298       $('createAccount').hidden = !data.createAccount;
299       $('guestSignin').hidden = !data.guestSignin;
300       $('createManagedUserPane').hidden = !data.managedUsersEnabled;
302       $('createManagedUserLinkPlaceholder').hidden =
303           !data.managedUsersCanCreate;
304       $('createManagedUserNoManagerText').hidden = data.managedUsersCanCreate;
305       $('createManagedUserNoManagerText').textContent =
306           data.managedUsersRestrictionReason;
308       this.isShowUsers_ = data.isShowUsers;
309       this.updateCancelButtonState();
311       // Sign-in right panel is hidden if all of its items are hidden.
312       var noRightPanel = $('gaia-signin-reason').hidden &&
313                          $('createAccount').hidden &&
314                          $('guestSignin').hidden &&
315                          $('createManagedUserPane').hidden;
316       this.classList.toggle('no-right-panel', noRightPanel);
317       if (Oobe.getInstance().currentScreen === this)
318         Oobe.getInstance().updateScreenSize(this);
319     },
321     /**
322      * Sends the authenticated user's e-mail address to the auth extension.
323      * @param {number} attemptToken The opaque token provided to
324      *     onRetrieveAuthenticatedUserEmail_.
325      * @param {string} email The authenticated user's e-mail address.
326      */
327     setAuthenticatedUserEmail: function(attemptToken, email) {
328       if (!email)
329         this.showFatalAuthError();
330       else
331         this.gaiaAuthHost_.setAuthenticatedUserEmail(attemptToken, email);
332     },
334     /**
335      * Updates [Cancel] button state. Allow cancellation of screen only when
336      * user pods can be displayed.
337      */
338     updateCancelButtonState: function() {
339       this.cancelAllowed_ = this.isShowUsers_ && $('pod-row').pods.length;
340       $('login-header-bar').allowCancel = this.cancelAllowed_;
341     },
343     /**
344      * Whether the current auth flow is SAML.
345      */
346     isSAML: function() {
347        return this.gaiaAuthHost_.authFlow ==
348            cr.login.GaiaAuthHost.AuthFlow.SAML;
349     },
351     /**
352      * Invoked when the authFlow property is changed no the gaia host.
353      * @param {Event} e Property change event.
354      */
355     onAuthFlowChange_: function(e) {
356       var isSAML = this.isSAML();
358       if (isSAML) {
359         $('saml-notice-message').textContent = loadTimeData.getStringF(
360             'samlNotice',
361             this.gaiaAuthHost_.authDomain);
362       }
364       this.classList.toggle('saml', isSAML);
365       $('saml-notice-container').hidden = !isSAML;
367       if (Oobe.getInstance().currentScreen === this) {
368         Oobe.getInstance().updateScreenSize(this);
369         $('login-header-bar').allowCancel = isSAML || this.cancelAllowed_;
370       }
371     },
373     /**
374      * Invoked when the auth host emits 'ready' event.
375      * @private
376      */
377     onAuthReady_: function() {
378       this.loading = false;
379       this.clearLoadingTimer_();
381       // Show deferred error bubble.
382       if (this.errorBubble_) {
383         this.showErrorBubble(this.errorBubble_[0], this.errorBubble_[1]);
384         this.errorBubble_ = undefined;
385       }
387       chrome.send('loginWebuiReady');
388       chrome.send('loginVisible', ['gaia-signin']);
390       // Warm up the user images screen.
391       Oobe.getInstance().preloadScreen({id: SCREEN_USER_IMAGE_PICKER});
392     },
394     /**
395      * Invoked when the user has successfully authenticated via SAML and the
396      * auth host needs to retrieve the user's e-mail.
397      * @param {number} attemptToken Opaque token to be passed to
398      *     setAuthenticatedUserEmail along with the e-mail address.
399      * @param {boolean} apiUsed Whether the principals API was used during
400      *     authentication.
401      * @private
402      */
403     onRetrieveAuthenticatedUserEmail_: function(attemptToken, apiUsed) {
404       if (apiUsed) {
405         // If the principals API was used, report this to the C++ backend so
406         // that statistics can be kept. If password scraping was used instead,
407         // there is no need to inform the C++ backend at this point: Either
408         // onAuthNoPassword_ or onAuthConfirmPassword_ will be called in a
409         // moment, both of which imply to the backend that the API was not used.
410         chrome.send('usingSAMLAPI');
411       }
412       chrome.send('retrieveAuthenticatedUserEmail', [attemptToken]);
413     },
415     /**
416      * Invoked when the user has successfully authenticated via SAML, the
417      * principals API was not used and the auth host needs the user to confirm
418      * the scraped password.
419      * @param {number} passwordCount The number of passwords that were scraped.
420      * @private
421      */
422     onAuthConfirmPassword_: function(passwordCount) {
423       this.loading = true;
424       Oobe.getInstance().headerHidden = false;
426       if (this.samlPasswordConfirmAttempt_ == 0)
427         chrome.send('scrapedPasswordCount', [passwordCount]);
429       if (this.samlPasswordConfirmAttempt_ < 2) {
430         login.ConfirmPasswordScreen.show(
431             this.samlPasswordConfirmAttempt_,
432             this.onConfirmPasswordCollected_.bind(this));
433       } else {
434         chrome.send('scrapedPasswordVerificationFailed');
435         this.showFatalAuthError();
436       }
437     },
439     /**
440      * Invoked when the confirm password screen is dismissed.
441      * @private
442      */
443     onConfirmPasswordCollected_: function(password) {
444       this.samlPasswordConfirmAttempt_++;
445       this.gaiaAuthHost_.verifyConfirmedPassword(password);
447       // Shows signin UI again without changing states.
448       Oobe.showScreen({id: SCREEN_GAIA_SIGNIN});
449     },
451     /**
452      * Inovked when the user has successfully authenticated via SAML, the
453      * principals API was not used and no passwords could be scraped.
454      * @param {string} email The authenticated user's e-mail.
455      */
456     onAuthNoPassword_: function(email) {
457       this.showFatalAuthError();
458       chrome.send('scrapedPasswordCount', [0]);
459     },
461     /**
462      * Shows the fatal auth error.
463      */
464     showFatalAuthError: function() {
465       login.FatalErrorScreen.show(Oobe.showSigninUI);
466     },
468     /**
469      * Invoked when auth is completed successfully.
470      * @param {!Object} credentials Credentials of the completed authentication.
471      * @private
472      */
473     onAuthCompleted_: function(credentials) {
474       if (credentials.useOffline) {
475         this.email = credentials.email;
476         chrome.send('authenticateUser',
477                     [credentials.email, credentials.password]);
478       } else if (credentials.authCode) {
479         chrome.send('completeAuthentication',
480                     [credentials.email,
481                      credentials.password,
482                      credentials.authCode]);
483       } else {
484         chrome.send('completeLogin',
485                     [credentials.email,
486                      credentials.password,
487                      credentials.usingSAML]);
488       }
490       this.loading = true;
491       // Now that we're in logged in state header should be hidden.
492       Oobe.getInstance().headerHidden = true;
493       // Clear any error messages that were shown before login.
494       Oobe.clearErrors();
495     },
497     /**
498      * Clears input fields and switches to input mode.
499      * @param {boolean} takeFocus True to take focus.
500      * @param {boolean} forceOnline Whether online sign-in should be forced.
501      * If |forceOnline| is false previously used sign-in type will be used.
502      */
503     reset: function(takeFocus, forceOnline) {
504       // Reload and show the sign-in UI if needed.
505       if (takeFocus) {
506         if (!forceOnline && this.isLocal) {
507           // Show 'Cancel' button to allow user to return to the main screen
508           // (e.g. this makes sense when connection is back).
509           Oobe.getInstance().headerHidden = false;
510           $('login-header-bar').signinUIState = SIGNIN_UI_STATE.GAIA_SIGNIN;
511           // Do nothing, since offline version is reloaded after an error comes.
512         } else {
513           Oobe.showSigninUI();
514         }
515       }
516     },
518     /**
519      * Reloads extension frame.
520      */
521     doReload: function() {
522       this.error_ = 0;
523       this.gaiaAuthHost_.reload();
524       this.loading = true;
525       this.startLoadingTimer_();
526     },
528     /**
529      * Updates localized content of the screen that is not updated via template.
530      */
531     updateLocalizedContent: function() {
532       $('createAccount').innerHTML = loadTimeData.getStringF(
533           'createAccount',
534           '<a id="createAccountLink" class="signin-link" href="#">',
535           '</a>');
536       $('guestSignin').innerHTML = loadTimeData.getStringF(
537           'guestSignin',
538           '<a id="guestSigninLink" class="signin-link" href="#">',
539           '</a>');
540       $('createManagedUserLinkPlaceholder').innerHTML = loadTimeData.getStringF(
541             'createLocallyManagedUser',
542             '<a id="createManagedUserLink" class="signin-link" href="#">',
543             '</a>');
544       $('createAccountLink').addEventListener('click', function(e) {
545         chrome.send('createAccount');
546         e.preventDefault();
547       });
548       $('guestSigninLink').addEventListener('click', function(e) {
549         chrome.send('launchIncognito');
550         e.preventDefault();
551       });
552       $('createManagedUserLink').addEventListener('click', function(e) {
553         chrome.send('showLocallyManagedUserCreationScreen');
554         e.preventDefault();
555       });
556     },
558     /**
559      * Shows sign-in error bubble.
560      * @param {number} loginAttempts Number of login attemps tried.
561      * @param {HTMLElement} content Content to show in bubble.
562      */
563     showErrorBubble: function(loginAttempts, error) {
564       if (this.isLocal) {
565         $('add-user-button').hidden = true;
566         $('cancel-add-user-button').hidden = false;
567         // Reload offline version of the sign-in extension, which will show
568         // error itself.
569         chrome.send('offlineLogin', [this.email]);
570       } else if (!this.loading) {
571         // We want to show bubble near "Email" field, but we can't calculate
572         // it's position because it is located inside iframe. So we only
573         // can hardcode some constants.
574         /** @const */ var ERROR_BUBBLE_OFFSET = 84;
575         /** @const */ var ERROR_BUBBLE_PADDING = 0;
576         $('bubble').showContentForElement($('login-box'),
577                                           cr.ui.Bubble.Attachment.LEFT,
578                                           error,
579                                           ERROR_BUBBLE_OFFSET,
580                                           ERROR_BUBBLE_PADDING);
581       } else {
582         // Defer the bubble until the frame has been loaded.
583         this.errorBubble_ = [loginAttempts, error];
584       }
585     },
587     /**
588      * Called when user canceled signin.
589      */
590     cancel: function() {
591       if (!this.cancelAllowed_) {
592         // In OOBE signin screen, cancel is not allowed because there is
593         // no other screen to show. If user is in middle of a saml flow,
594         // reset signin screen to get out of the saml flow.
595         if (this.isSAML())
596           Oobe.resetSigninUI(true);
598         return;
599       }
601       $('pod-row').loadLastWallpaper();
602       Oobe.showScreen({id: SCREEN_ACCOUNT_PICKER});
603       Oobe.resetSigninUI(true);
604     },
606     /**
607      * Handler for iframe's error notification coming from the outside.
608      * For more info see C++ class 'WebUILoginView' which calls this method.
609      * @param {number} error Error code.
610      * @param {string} url The URL that failed to load.
611      */
612     onFrameError: function(error, url) {
613       // Chrome OS requires that the entire authentication flow use https. If
614       // GAIA attempts to redirect to an http URL, the load will be blocked by
615       // CSP. Show a fatal error in this case.
616       // Some tests deviate from the above by disabling the CSP and using a
617       // mock GAIA implementation served over http. If an http URL fails to load
618       // in such a test, it has nothing to do with CSP and should not cause a
619       // fatal error to be shown.
620       if (error == NET_ERROR_ABORTED &&
621           url.indexOf('http://') == 0 &&
622           this.gaiaAuthParams_.gaiaUrl.indexOf('https://') == 0) {
623         error = NET_ERROR_DISALLOWED_URL_SCHEME;
624         this.showFatalAuthError();
625       }
626       this.error_ = error;
627       chrome.send('frameLoadingCompleted', [this.error_]);
628     },
629   };