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.
6 * @fileoverview Oobe signin screen implementation.
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;
28 'updateAuthExtension',
29 'setAuthenticatedUserEmail',
32 'updateCancelButtonState'
36 * Frame loading error code (0 - no error).
43 * Saved gaia auth host load params.
47 gaiaAuthParams_: null,
50 * Whether local version of Gaia page is used.
57 * Email of the user, which is logging in using offline mode.
63 * Timer id of pending load.
67 loadingTimer_: undefined,
70 * Whether user can cancel Gaia screen.
74 cancelAllowed_: undefined,
77 * Whether we should show user pods on the login screen.
81 isShowUsers_: undefined,
84 * SAML password confirmation attempt count.
87 samlPasswordConfirmAttempt_: 0,
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]);
109 this.updateLocalizedContent();
113 * Header text of the screen.
117 return loadTimeData.getString('signinScreenTitle');
121 * Returns true if local version of Gaia is used.
125 return this.isLocal_;
129 * Sets whether local version of Gaia is used.
130 * @param {boolean} value Whether local version of Gaia is used.
133 this.isLocal_ = value;
134 chrome.send('updateOfflineLogin', [value]);
138 * Shows/hides loading UI.
139 * @param {boolean} show True to show loading UI.
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;
151 * Handler for Gaia loading suspiciously long timeout.
154 onLoadingSuspiciouslyLong_: function() {
155 if (this != Oobe.getInstance().currentScreen)
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) *
165 * Handler for Gaia loading timeout.
168 onLoadingTimeOut_: function() {
169 this.loadingTimer_ = undefined;
170 chrome.send('showLoadingTimeoutError');
174 * Clears loading timer.
177 clearLoadingTimer_: function() {
178 if (this.loadingTimer_) {
179 window.clearTimeout(this.loadingTimer_);
180 this.loadingTimer_ = undefined;
185 * Sets up loading timer.
188 startLoadingTimer_: function() {
189 this.clearLoadingTimer_();
190 this.loadingTimer_ = window.setTimeout(
191 this.onLoadingSuspiciouslyLong_.bind(this),
192 GAIA_LOADING_PORTAL_SUSSPECT_TIME_SEC * 1000);
196 * Whether Gaia is loading.
200 return !$('gaia-loading').hidden;
202 set loading(loading) {
203 if (loading == this.loading)
206 this.showLoadingUI_(loading);
210 * Event handler that is invoked just before the frame is shown.
211 * @param {string} data Screen init payload. Url of auth extension start
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']);
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;
233 * Event handler that is invoked just before the screen is hidden.
235 onBeforeHide: function() {
236 chrome.send('loginUIStateChanged', ['gaia-signin', false]);
237 $('login-header-bar').signinUIState = SIGNIN_UI_STATE.HIDDEN;
241 * Loads the authentication extension into the iframe.
242 * @param {Object} data Extension parameters bag.
245 loadAuthExtension: function(data) {
246 this.isLocal = data.isLocal;
250 this.classList.toggle('saml', false);
251 this.samlPasswordConfirmAttempt_ = 0;
253 this.updateAuthExtension(data);
256 for (var i in cr.login.GaiaAuthHost.SUPPORTED_PARAMS) {
257 var name = cr.login.GaiaAuthHost.SUPPORTED_PARAMS[i];
259 params[name] = data[name];
262 if (data.localizedStrings)
263 params.localizedStrings = data.localizedStrings;
265 if (data.forceReload ||
266 JSON.stringify(this.gaiaAuthParams_) != JSON.stringify(params)) {
268 this.gaiaAuthHost_.load(data.useOffline ?
269 cr.login.GaiaAuthHost.AuthMode.OFFLINE :
270 cr.login.GaiaAuthHost.AuthMode.DEFAULT,
272 this.onAuthCompleted_.bind(this));
273 this.gaiaAuthParams_ = params;
276 this.startLoadingTimer_();
277 } else if (this.loading && this.error_) {
278 // An error has occurred, so trying to reload.
284 * Updates the authentication extension with new parameters, if needed.
285 * @param {Object} data New extension parameters bag.
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;
295 reasonLabel.hidden = true;
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);
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.
327 setAuthenticatedUserEmail: function(attemptToken, email) {
329 this.showFatalAuthError();
331 this.gaiaAuthHost_.setAuthenticatedUserEmail(attemptToken, email);
335 * Updates [Cancel] button state. Allow cancellation of screen only when
336 * user pods can be displayed.
338 updateCancelButtonState: function() {
339 this.cancelAllowed_ = this.isShowUsers_ && $('pod-row').pods.length;
340 $('login-header-bar').allowCancel = this.cancelAllowed_;
344 * Whether the current auth flow is SAML.
347 return this.gaiaAuthHost_.authFlow ==
348 cr.login.GaiaAuthHost.AuthFlow.SAML;
352 * Invoked when the authFlow property is changed no the gaia host.
353 * @param {Event} e Property change event.
355 onAuthFlowChange_: function(e) {
356 var isSAML = this.isSAML();
359 $('saml-notice-message').textContent = loadTimeData.getStringF(
361 this.gaiaAuthHost_.authDomain);
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_;
374 * Invoked when the auth host emits 'ready' event.
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;
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});
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
403 onRetrieveAuthenticatedUserEmail_: function(attemptToken, 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');
412 chrome.send('retrieveAuthenticatedUserEmail', [attemptToken]);
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.
422 onAuthConfirmPassword_: function(passwordCount) {
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));
434 chrome.send('scrapedPasswordVerificationFailed');
435 this.showFatalAuthError();
440 * Invoked when the confirm password screen is dismissed.
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});
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.
456 onAuthNoPassword_: function(email) {
457 this.showFatalAuthError();
458 chrome.send('scrapedPasswordCount', [0]);
462 * Shows the fatal auth error.
464 showFatalAuthError: function() {
465 login.FatalErrorScreen.show(Oobe.showSigninUI);
469 * Invoked when auth is completed successfully.
470 * @param {!Object} credentials Credentials of the completed authentication.
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',
481 credentials.password,
482 credentials.authCode]);
484 chrome.send('completeLogin',
486 credentials.password,
487 credentials.usingSAML]);
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.
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.
503 reset: function(takeFocus, forceOnline) {
504 // Reload and show the sign-in UI if needed.
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.
519 * Reloads extension frame.
521 doReload: function() {
523 this.gaiaAuthHost_.reload();
525 this.startLoadingTimer_();
529 * Updates localized content of the screen that is not updated via template.
531 updateLocalizedContent: function() {
532 $('createAccount').innerHTML = loadTimeData.getStringF(
534 '<a id="createAccountLink" class="signin-link" href="#">',
536 $('guestSignin').innerHTML = loadTimeData.getStringF(
538 '<a id="guestSigninLink" class="signin-link" href="#">',
540 $('createManagedUserLinkPlaceholder').innerHTML = loadTimeData.getStringF(
541 'createLocallyManagedUser',
542 '<a id="createManagedUserLink" class="signin-link" href="#">',
544 $('createAccountLink').addEventListener('click', function(e) {
545 chrome.send('createAccount');
548 $('guestSigninLink').addEventListener('click', function(e) {
549 chrome.send('launchIncognito');
552 $('createManagedUserLink').addEventListener('click', function(e) {
553 chrome.send('showLocallyManagedUserCreationScreen');
559 * Shows sign-in error bubble.
560 * @param {number} loginAttempts Number of login attemps tried.
561 * @param {HTMLElement} content Content to show in bubble.
563 showErrorBubble: function(loginAttempts, error) {
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
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,
580 ERROR_BUBBLE_PADDING);
582 // Defer the bubble until the frame has been loaded.
583 this.errorBubble_ = [loginAttempts, error];
588 * Called when user canceled signin.
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.
596 Oobe.resetSigninUI(true);
601 $('pod-row').loadLastWallpaper();
602 Oobe.showScreen({id: SCREEN_ACCOUNT_PICKER});
603 Oobe.resetSigninUI(true);
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.
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();
627 chrome.send('frameLoadingCompleted', [this.error_]);