chromium/chrome/browser/resources/gaia_auth_host/authenticator.js

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// clang-format off
// <if expr="not chromeos_ash">
import {assert} from 'chrome://resources/js/assert.js';
import {sendWithPromise} from 'chrome://resources/js/cr.js';
import {$, appendParam} from 'chrome://resources/js/util.js';
// </if>
// <if expr="chromeos_ash">
import {assert} from 'chrome://resources/ash/common/assert.js';
import {sendWithPromise} from 'chrome://resources/ash/common/cr.m.js';
import {NativeEventTarget as EventTarget} from 'chrome://resources/ash/common/event_target.js';
import {$, appendParam} from 'chrome://resources/ash/common/util.js';

// </if>

import {OnHeadersReceivedDetails, SamlHandler} from './saml_handler.js';
import {PasswordAttributes} from './saml_password_attributes.js';
import {WebviewEventManager} from './webview_event_manager.js';
//clang-format on

/**
 * @fileoverview An UI component to authenticate to Chrome. The component hosts
 * IdP web pages in a webview. A client who is interested in monitoring
 * authentication events should subscribe itself via addEventListener(). After
 * initialization, call {@code load} to start the authentication flow.
 *
 * See go/cros-auth-design for details on Google API.
 */

  /**
   * Individual sync trusted vault key.
   * @typedef {{
   *   keyMaterial: ArrayBuffer,
   *   version: number,
   * }}
   */
export let SyncTrustedVaultKey;

/**
 * Individual sync trusted recovery method.
 * @typedef {{
 *   publicKey: ArrayBuffer,
 *   type: number,
 * }}
 */
export let SyncTrustedRecoveryMethod;

/**
 * Sync trusted vault encryption keys optionally passed with 'authCompleted'
 * message.
 * @typedef {{
 *   obfuscatedGaiaId: string,
 *   encryptionKeys: Array<SyncTrustedVaultKey>,
 *   trustedRecoveryMethods: Array<SyncTrustedRecoveryMethod>
 * }}
 */
export let SyncTrustedVaultKeys;

/**
 * Credentials passed with 'authCompleted' message.
 * `isAvailableInArc` field is optional and is used only on Chrome OS.
 * @typedef {{
 *   email: string,
 *   gaiaId: string,
 *   password: string,
 *   usingSAML: boolean,
 *   publicSAML: boolean,
 *   skipForNow: boolean,
 *   sessionIndex: string,
 *   trusted: boolean,
 *   services: Array,
 *   passwordAttributes: !PasswordAttributes,
 *   syncTrustedVaultKeys: !SyncTrustedVaultKeys,
 *   isAvailableInArc: (boolean|undefined),
 * }}
 */
export let AuthCompletedCredentials;

/**
 * Parameters for the authorization flow.
 * @typedef {{
 *   hl: string,
 *   gaiaUrl: string,
 *   authMode: AuthMode,
 *   isLoginPrimaryAccount: boolean,
 *   email: string,
 *   constrained: string,
 *   platformVersion: string,
 *   readOnlyEmail: boolean,
 *   service: string,
 *   dontResizeNonEmbeddedPages: boolean,
 *   clientId: string,
 *   clientVersion: (string|undefined),
 *   gaiaPath: string,
 *   emailDomain: string,
 *   showTos: string,
 *   extractSamlPasswordAttributes: boolean,
 *   flow: string,
 *   ignoreCrOSIdpSetting: boolean,
 *   enableGaiaActionButtons: boolean,
 *   forceDarkMode: boolean,
 *   enterpriseEnrollmentDomain: string,
 *   samlAclUrl: string,
 *   isSupervisedUser: boolean,
 *   isDeviceOwner: boolean,
 *   needPassword: (boolean|undefined),
 *   ssoProfile: string,
 *   urlParameterToAutofillSAMLUsername: string,
 *   frameUrl: URL,
 *   isFirstUser : (boolean|undefined),
 *   recordAccountCreation : (boolean|undefined),
 *   autoReloadAttempts : number,
 * }}
 */
export let AuthParams;

const SIGN_IN_HEADER = 'google-accounts-signin';
const EMBEDDED_FORM_HEADER = 'google-accounts-embedded';
const SERVICE_ID = 'chromeoslogin';
const BLANK_PAGE_URL = 'about:blank';

const GAIA_DONE_ELAPSED_TIME = 'ChromeOS.Gaia.Done.ElapsedTime';
const GAIA_CREATE_ACCOUNT_FIRST_USER =
      'ChromeOS.Gaia.CreateAccount.IsFirstUser';
const GAIA_DONE_OOBE_NEW_ACCOUNT =
      'ChromeOS.Gaia.Done.Oobe.NewAccount';

// Metric names for messages we get from Gaia.
const GAIA_MESSAGE_SAML_USER_INFO = 'ChromeOS.Gaia.Message.Saml.UserInfo';
const GAIA_MESSAGE_GAIA_USER_INFO = 'ChromeOS.Gaia.Message.Gaia.UserInfo';
const GAIA_MESSAGE_SAML_CLOSE_VIEW = 'ChromeOS.Gaia.Message.Saml.CloseView';
const GAIA_MESSAGE_GAIA_CLOSE_VIEW = 'ChromeOS.Gaia.Message.Gaia.CloseView';

/**
 * The source URL parameter for the constrained signin flow.
 */
export const CONSTRAINED_FLOW_SOURCE = 'chrome';

/**
 * Enum for the authorization mode, must match AuthMode defined in
 * chrome/browser/ui/webui/inline_login_ui.cc.
 * @enum {number}
 */
export const AuthMode = {
  DEFAULT: 0,
  OFFLINE: 1,
  DESKTOP: 2,
};

/**
 * Enum for the authorization type.
 * @enum {number}
 */
export const AuthFlow = {
  DEFAULT: 0,
  SAML: 1,
};

/**
 * Supported Authenticator params.
 * @type {!Array<string>}
 * @const
 */
export const SUPPORTED_PARAMS = [
  'gaiaId',        // Obfuscated GAIA ID to skip the email prompt page
                   // during the re-auth flow.
  'gaiaUrl',       // Gaia url to use.
  'gaiaPath',      // Gaia path to use without a leading slash.
  'hl',            // Language code for the user interface.
  'service',       // Name of Gaia service.
  'frameUrl',      // Initial frame URL to use. If empty defaults to
                   // gaiaUrl.
  'constrained',   // Whether authentication happens in a constrained
                   // window.
  'clientId',      // Chrome client id.
  'needPassword',  // Whether the host is interested in getting a password.
                   // If this set to |false|, |confirmPasswordCallback| is
                   // not called before dispatching |authCopleted|.
                   // Default is |true|.
  'flow',          // One of 'default', 'enterprise', or
                   // 'cfm' or 'enterpriseLicense'.
  'enterpriseDomainManager',     // Manager of the current domain. Can be
                                 // either a domain name (foo.com) or an email
                                 // address ([email protected]).
  'enterpriseEnrollmentDomain',  // Domain in which hosting device is (or
                                 // should be) enrolled.
  'emailDomain',                 // Value used to prefill domain for email.
  'chromeType',                  // Type of Chrome OS device, e.g. "chromebox".
  'clientVersion',               // Version of the Chrome build.
  'platformVersion',             // Version of the OS build.
  'releaseChannel',              // Installation channel.
  'endpointGen',                 // Current endpoint generation.
  'menuEnterpriseEnrollment',    // Enables "Enterprise enrollment" menu item.
  'lsbReleaseBoard',             // Chrome OS Release board name
  'isFirstUser',                 // True if this is non-enterprise device,
                                 // and there are no users yet.
  'obfuscatedOwnerId',           // Obfuscated device owner ID, if needed.
  'extractSamlPasswordAttributes',  // If enabled attempts to extract password
                                    // attributes from the SAML response.
  'ignoreCrOSIdpSetting',           // If set to true, causes Gaia to ignore 3P
                                    // SAML IdP SSO redirection policies (and
                                    // redirect to SAML IdPs by default).
  'ssoProfile',  // An identifier for the device's managing OU's
                 // SAML SSO setting. Used by the login screen to
                 // pass to Gaia.
  // The email can be passed to Gaia to let it know which user is trying to
  // sign in. Gaia behavior can be different depending on the `gaiaPath`: it
  // can either simply prefill the email field, but still allow modifying it,
  // or it can proceed straight to the authentication challenge for the
  // corresponding account, not allowing the user to modify the email.
  'email',
   // Determines which URL parameter will be used to pass the email to Gaia.
   // TODO(b/292087570): misleading name, should be either renamed or
   // removed completely (need to confirm if email_hint URL parameter
   // is still relevant for some flows).
  'readOnlyEmail',
  'realm',
  // If the authentication is done via external IdP, 'startsOnSamlPage'
  // indicates whether the flow should start on the IdP page.
  'startsOnSamlPage',
  // SAML assertion consumer URL, used to detect when Gaia-less SAML flows end
  // (e.g. for SAML managed guest sessions).
  'samlAclUrl',
  'isSupervisedUser',  // True if the user is supervised user.
  'isDeviceOwner',     // True if the user is device owner.
  'doSamlRedirect',    // True if the authentication is done via external IdP.
  'rart',              // Encrypted reauth request token.
  // Url parameter name for SAML IdP web page which is used to autofill the
  // username.
  'urlParameterToAutofillSAMLUsername',
  'forceDarkMode',
  // A tri-state value which indicates the support level for passwordless login.
  // Refer to `GaiaView::PasswordlessSupportLevel` for details.
  'pwl',
  // Control if the account creation during sign in flow should be handled.
  'recordAccountCreation',
  // Url parameter for the number of automatic reloads done to the
  // authentication flow to avoid login page timeout. Added for
  // `DeviceAuthenticationFlowAutoReloadInterval` policy.
  'autoReloadAttempts',
];

// Timeout in ms to wait for the message from Gaia indicating end of the flow.
// Could be userInfo (The message is used to extract user services and to
// define whether or not the account is a child one) or closeView (specific
// message to indicate the end of the flow).
const GAIA_DONE_WAIT_TIMEOUT_MS = 5 * 1000;

/**
 * Extract domain name from an URL.
 * @param {string} url An URL string.
 * @return {string} The host name of the URL.
 */
function extractDomain(url) {
  const a = document.createElement('a');
  a.href = url;
  return a.hostname;
}

/**
 * Handlers for the HTML5 messages received from Gaia.
 * Each handler is a function that receives the Authenticator as 'this',
 * and the data field of the HTML5 Payload.
 */
const messageHandlers = {
  'attemptLogin'(msg) {
    this.setEmail_(msg.email);
    if (this.authMode === AuthMode.DESKTOP) {
      this.password_ = msg.password;
    }

    // We need to dispatch only first event, before user enters password.
    this.dispatchEvent(new CustomEvent('attemptLogin', {detail: msg.email}));
  },
  'dialogShown'(msg) {
    this.dispatchEvent(new Event('dialogShown'));
  },
  'dialogHidden'(msg) {
    this.dispatchEvent(new Event('dialogHidden'));
  },
  'backButton'(msg) {
    this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show}));
  },
  'getAccounts'(msg) {
    this.dispatchEvent(new Event('getAccounts'));
  },
  'showView'(msg) {
    this.dispatchEvent(new Event('showView'));
  },
  'menuItemClicked'(msg) {
    this.dispatchEvent(new CustomEvent('menuItemClicked', {detail: msg.item}));
  },
  'identifierEntered'(msg) {
    this.setEmail_(msg.accountIdentifier);
    this.dispatchEvent(new CustomEvent(
        'identifierEntered',
        {detail: {accountIdentifier: msg.accountIdentifier}}));
  },
  'userInfo'(msg) {
    this.services_ = msg.services;
    this.servicesProvided_ = true;
    if (!this.authCompletedFired_) {
      const metric = this.authFlow === AuthFlow.SAML ?
          GAIA_MESSAGE_SAML_USER_INFO :
          GAIA_MESSAGE_GAIA_USER_INFO;
      chrome.send('metricsHandler:recordBooleanHistogram', [metric, true]);
    }
    if (this.email_ && this.gaiaId_ && this.sessionIndex_) {
      this.maybeCompleteAuth_();
    }
  },
  'showIncognito'(msg) {
    this.dispatchEvent(new Event('showIncognito'));
  },
  'setPrimaryActionLabel'(msg) {
    if (!this.enableGaiaActionButtons_) {
      return;
    }
    this.dispatchEvent(
        new CustomEvent('setPrimaryActionLabel', {detail: msg.value}));
  },
  'setPrimaryActionEnabled'(msg) {
    if (!this.enableGaiaActionButtons_) {
      return;
    }
    this.dispatchEvent(
        new CustomEvent('setPrimaryActionEnabled', {detail: msg.value}));
  },
  'setSecondaryActionLabel'(msg) {
    if (!this.enableGaiaActionButtons_) {
      return;
    }
    this.dispatchEvent(
        new CustomEvent('setSecondaryActionLabel', {detail: msg.value}));
  },
  'setSecondaryActionEnabled'(msg) {
    if (!this.enableGaiaActionButtons_) {
      return;
    }
    this.dispatchEvent(
        new CustomEvent('setSecondaryActionEnabled', {detail: msg.value}));
  },
  'setAllActionsEnabled'(msg) {
    if (!this.enableGaiaActionButtons_) {
      return;
    }
    this.dispatchEvent(
        new CustomEvent('setAllActionsEnabled', {detail: msg.value}));
  },
  'removeUserByEmail'(msg) {
    this.dispatchEvent(
        new CustomEvent('removeUserByEmail', {detail: msg.email}));
  },
  'exit'(msg) {
    this.dispatchEvent(new CustomEvent('exit'));
  },
  'syncTrustedVaultKeys'(msg) {
    this.syncTrustedVaultKeys_ = msg.value;
  },
  'closeView'(msg) {
    if (!this.authCompletedFired_) {
      if (!this.services_) {
        console.error('Authenticator: UserInfo should come before closeView');
      }
      const metric = this.authFlow === AuthFlow.SAML ?
          GAIA_MESSAGE_SAML_CLOSE_VIEW :
          GAIA_MESSAGE_GAIA_CLOSE_VIEW;
      chrome.send('metricsHandler:recordBooleanHistogram', [metric, true]);
    }

    this.closeViewReceived_ = true;
    if (this.email_ && this.gaiaId_ && this.sessionIndex_) {
      this.maybeCompleteAuth_();
    }
  },
  'getDeviceId'(msg) {
    this.dispatchEvent(new Event('getDeviceId'));
  },
};

/**
 * Old or not supported on Chrome OS messages.
 * @type {!Array<string>}
 * @const
 */
const IGNORED_MESSAGES_FROM_GAIA = [
  'clearOldAttempts',
  'showConfirmCancel',
];

/**
 * Initializes the authenticator component.
 */
export class Authenticator extends EventTarget {
  /**
   * @param {!WebView|string} webview The webview element or its ID to host
   *     IdP web pages.
   */
  constructor(webview) {
    super();

    /** @private {AuthFlow} The current auth flow of the hosted page.*/
    this.authFlow_ = AuthFlow.DEFAULT;
    /** @private {string} The domain name of the current auth page. */
    this.authDomain_ = '';
    /** @private {boolean}  Whether media access was requested. */
    this.videoEnabled_ = false;

    this.isLoaded_ = false;
    this.email_ = null;
    this.password_ = null;
    this.gaiaId_ = null, this.sessionIndex_ = null;
    this.skipForNow_ = false;
    /** @type {AuthMode} */
    this.authMode = AuthMode.DEFAULT;
    this.dontResizeNonEmbeddedPages = false;
    this.isFirstUser_ = false;
    this.isNewAccount = false;

    /**
     * @type {!SamlHandler|undefined}
     * @private
     */
    this.samlHandler_ = undefined;
    this.idpOrigin_ = null;
    this.initialFrameUrl_ = null;
    this.reloadUrl_ = null;
    this.trusted_ = true;
    this.readyFired_ = false;
    this.authCompletedFired_ = false;
    /**
     * @private {WebView|undefined}
     */
    this.webview_ = typeof webview === 'string' ?
        /** @type {WebView} */ ($(webview)) :
        webview;
    assert(this.webview_);
    this.enableGaiaActionButtons_ = false;
    this.webviewEventManager_ = new WebviewEventManager();

    this.clientId_ = null;

    this.confirmPasswordCallback = null;
    this.noPasswordCallback = null;
    this.onePasswordCallback = null;
    this.insecureContentBlockedCallback = null;
    this.samlApiUsedCallback = null;
    this.recordSamlProviderCallback = null;
    this.missingGaiaInfoCallback = null;
    this.needPassword = true;
    this.services_ = null;
    this.servicesProvided_ = false;
    this.waitApiPasswordConfirm_ = false;
    this.gaiaDoneTimer_ = null;
    /** @private {boolean} */
    this.isConstrainedWindow_ = false;
    this.samlAclUrl_ = null;
    /** @private {?SyncTrustedVaultKeys} */
    this.syncTrustedVaultKeys_ = null;
    this.closeViewReceived_ = false;
    this.gaiaStartTime = null;

    window.addEventListener(
        'message', e => this.onMessageFromWebview_(e), false);
    window.addEventListener('focus', () => this.onFocus_(), false);
    window.addEventListener('popstate', e => this.onPopState_(e), false);

    /**
     * @type {boolean}
     * @private
     */
    this.isDomLoaded_ = document.readyState !== 'loading';
    if (this.isDomLoaded_) {
      this.initializeAfterDomLoaded_();
    } else {
      document.addEventListener(
          'DOMContentLoaded', () => this.initializeAfterDomLoaded_());
    }
  }

  /** @return {AuthFlow} */
  get authFlow() {
    return this.authFlow_;
  }

  /**
   * Dispatches 'authFlowChange' event if the value changes.
   * @param {AuthFlow} value
   */
  set authFlow(value) {
    const previous = this.authFlow_;
    if (value !== previous) {
      this.authFlow_ = value;
      this.dispatchEvent(new CustomEvent('authFlowChange', {
        bubbles: true,
        composed: true,
        detail: {oldValue: previous, newValue: value},
      }));
    }
  }

  /** @return {string} */
  get authDomain() {
    return this.authDomain_;
  }

  /**
   * Dispatches 'authDomainChange' event if the value changes.
   * @param {string} domain
   */
  set authDomain(domain) {
    const previous = this.authDomain_;
    if (domain !== previous) {
      this.authDomain_ = domain;
      this.dispatchEvent(new CustomEvent('authDomainChange', {
        bubbles: true,
        composed: true,
        detail: {oldValue: previous, newValue: domain},
      }));
    }
  }

  /** @return {boolean} */
  get videoEnabled() {
    return this.videoEnabled_;
  }

  /**
   * Dispatches 'videoEnabledChange' event if the value changes.
   * @param {boolean} enabled
   */
  set videoEnabled(enabled) {
    const previous = this.videoEnabled_;
    if (enabled !== previous) {
      this.videoEnabled_ = enabled;
      this.dispatchEvent(new CustomEvent('videoEnabledChange', {
        bubbles: true,
        composed: true,
        detail: {oldValue: previous, newValue: enabled},
      }));
    }
  }

  /**
   * Reinitializes authentication parameters so that a failed login attempt
   * would not result in an infinite loop.
   */
  resetStates() {
    this.isLoaded_ = false;
    this.email_ = null;
    this.gaiaId_ = null;
    this.password_ = null;
    this.readyFired_ = false;
    this.skipForNow_ = false;
    this.sessionIndex_ = null;
    this.trusted_ = true;
    this.authFlow = AuthFlow.DEFAULT;
    this.samlHandler_.reset();
    this.videoEnabled = false;
    this.services_ = null;
    this.servicesProvided_ = false;
    this.waitApiPasswordConfirm_ = false;
    this.maybeClearGaiaTimeout_();
    this.syncTrustedVaultKeys_ = null;
    this.closeViewReceived_ = false;
    this.disableAllActions_();
  }

  /**
   * Resets the webview to the blank page.
   */
  resetWebview() {
    if (this.webview_.src && this.webview_.src !== BLANK_PAGE_URL) {
      this.webview_.src = BLANK_PAGE_URL;
    }
  }

  /**
   * Completes the part of the initialization that should happen after the
   * page's DOM has loaded.
   * @private
   */
  initializeAfterDomLoaded_() {
    this.isDomLoaded_ = true;
    this.bindToWebview_();
  }

  /**
   * Binds this authenticator to the current |webview_|.
   * @private
   */
  bindToWebview_() {
    assert(this.webview_);
    assert(this.webview_.request);
    assert(!this.samlHandler_);

    this.samlHandler_ =
        new SamlHandler(this.webview_, false /* startsOnSamlPage */);
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'insecureContentBlocked',
        e => this.onInsecureContentBlocked_(e));
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'authPageLoaded', e => this.onAuthPageLoaded_(e));
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'videoEnabled', () => this.videoEnabled = true);
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'apiPasswordAdded',
        e => this.onSamlApiPasswordAdded_(e));
    this.webviewEventManager_.addEventListener(
          this.samlHandler_, 'apiAccountCreated',
          e => this.onSamlApiAccountCreated_(e));
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'apiPasswordConfirmed',
        e => this.onSamlApiPasswordConfirmed_(e));
    this.webviewEventManager_.addEventListener(
        this.samlHandler_, 'challengeMachineKeyRequired',
        e => this.onChallengeMachineKeyRequired_(e));

    this.webviewEventManager_.addEventListener(
        this.webview_, 'droplink', e => this.onDropLink_(e));
    this.webviewEventManager_.addEventListener(
        this.webview_, 'newwindow', e => this.onNewWindow_(e));
    this.webviewEventManager_.addEventListener(
        this.webview_, 'contentload', e => this.onContentLoad_(e));
    this.webviewEventManager_.addEventListener(
        this.webview_, 'loadabort', e => this.onLoadAbort_(e));
    this.webviewEventManager_.addEventListener(
        this.webview_, 'loadcommit', e => this.onLoadCommit_(e));

    this.webviewEventManager_.addWebRequestEventListener(
        this.webview_.request.onCompleted,
        details => this.onRequestCompleted_(details),
        {urls: ['<all_urls>'], types: ['main_frame']}, ['responseHeaders']);
    this.webviewEventManager_.addWebRequestEventListener(
        this.webview_.request.onHeadersReceived,
        details => this.onHeadersReceived_(details),
        {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
        ['responseHeaders']);
  }

  /**
   * Unbinds this Authenticator from the currently bound webview.
   * @private
   */
  unbindFromWebview_() {
    assert(this.webview_);
    assert(this.samlHandler_);

    this.webviewEventManager_.removeAllListeners();

    this.webview_ = undefined;
    this.samlHandler_.unbindFromWebview();
    this.samlHandler_ = undefined;
  }

  /**
   * Re-binds to another webview.
   * @param {WebView} webview the new webview to be used by this
   *     Authenticator.
   * @private
   */
  rebindWebview_(webview) {
    if (!this.isDomLoaded_) {
      // We haven't bound to the previously set webview yet, so simply update
      // |webview_| to use the new element during the delayed initialization.
      this.webview_ = webview;
      return;
    }
    this.unbindFromWebview_();
    assert(!this.webview_);
    this.webview_ = webview;
    this.bindToWebview_();
  }

  /**
   * Copies attributes between nodes.
   * @param {!Element} fromNode source to copy attributes from
   * @param {!Element} toNode target to copy attributes to
   * @param {!Set<string>} skipAttributes specifies attributes to be skipped
   * @private
   */
  copyAttributes_(fromNode, toNode, skipAttributes) {
    for (let i = 0; i < fromNode.attributes.length; ++i) {
      const attribute = fromNode.attributes[i];
      if (!skipAttributes.has(attribute.nodeName)) {
        toNode.setAttribute(attribute.nodeName, attribute.nodeValue);
      }
    }
  }

  /**
   * Changes the 'partition' attribute of |webview_|. If |webview_| has
   * already navigated, this function re-creates it since the storage
   * partition of an active renderer process cannot change.
   * @param {string} newWebviewPartitionName the new partition
   */
  setWebviewPartition(newWebviewPartitionName) {
    if (!this.webview_.src) {
      // We have not navigated anywhere yet. Note that a webview's src
      // attribute does not allow a change back to "".
      this.webview_.partition = newWebviewPartitionName;
    } else if (this.webview_.partition !== newWebviewPartitionName) {
      // The webview has already navigated. We have to re-create it.
      const webivewParent = this.webview_.parentElement;

      // Copy all attributes except for partition and src from the previous
      // webview. Use the specified |newWebviewPartitionName|.
      const newWebview = document.createElement('webview');
      this.copyAttributes_(
          this.webview_, newWebview, new Set(['src', 'partition']));
      newWebview.partition = newWebviewPartitionName;

      webivewParent.replaceChild(newWebview, this.webview_);

      this.rebindWebview_(/** @type {WebView} */ (newWebview));
    }
  }

  /**
   * Loads the authenticator component with the given parameters.
   * @param {AuthMode} authMode Authorization mode.
   * @param {AuthParams} data Parameters for the authorization flow.
   */
  load(authMode, data) {
    this.authMode = authMode;
    this.resetStates();
    this.authCompletedFired_ = false;
    this.idpOrigin_ = data.gaiaUrl;
    this.isConstrainedWindow_ = data.constrained === '1';
    this.clientId_ = data.clientId;
    this.dontResizeNonEmbeddedPages = data.dontResizeNonEmbeddedPages;
    this.enableGaiaActionButtons_ = data.enableGaiaActionButtons;

    this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
    this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
    this.samlAclUrl_ = data.samlAclUrl;
    this.setEmail_(data.email);

    if (data.startsOnSamlPage) {
      this.samlHandler_.startsOnSamlPage = true;
    }

    // True if this is non-enterprise device and there are no users yet.
    this.isFirstUser_ = !!data.isFirstUser;

    // Enable or disable handling account create message from Gaia.
    this.samlHandler_.shouldHandleAccountCreationMessage =
        !!data.recordAccountCreation;

    // Don't block insecure content for desktop flow because it lands on
    // http. Otherwise, block insecure content as long as gaia is https.
    this.samlHandler_.blockInsecureContent =
        authMode !== AuthMode.DESKTOP && this.idpOrigin_.startsWith('https://');
    this.samlHandler_.extractSamlPasswordAttributes =
        data.extractSamlPasswordAttributes;
    this.samlHandler_.urlParameterToAutofillSAMLUsername =
        data.urlParameterToAutofillSAMLUsername;

    this.needPassword = !('needPassword' in data) || data.needPassword;

    this.webview_.contextMenus.onShow.addListener(function(e) {
      e.preventDefault();
    });

    this.webview_.src = this.reloadUrl_;
    this.isLoaded_ = true;
    this.isNewAccount = false;
  }

  /**
   * Reloads the authenticator component.
   */
  reload() {
    this.resetStates();
    this.authCompletedFired_ = false;
    this.webview_.src = this.reloadUrl_;
    this.isLoaded_ = true;
  }

  /**
   * Called in response to 'getAccounts' event.
   * @param {Array<string>} accounts list of emails
   */
  getAccountsResponse(accounts) {
    this.sendMessageToWebview('accountsListed', accounts);
  }

  /**
   * Called in response to 'getDeviceId' event.
   * @param {string} deviceId Device ID.
   */
  getDeviceIdResponse(deviceId) {
    this.sendMessageToWebview('deviceIdFetched', deviceId);
  }

  constructInitialFrameUrl_(data) {
    assert(this.idpOrigin_ !== undefined, "this.idpOrigin_ must be defined");
    assert(data.gaiaPath !== undefined, "data.gaiaPath must be defined");
    let url = this.idpOrigin_ + data.gaiaPath;

    if (data.doSamlRedirect) {
      url = appendParam(url, 'domain', data.enterpriseEnrollmentDomain);
      if (data.ssoProfile) {
        url = appendParam(url, 'sso_profile', data.ssoProfile);
      }
      url = appendParam(
          url, 'continue',
          data.gaiaUrl + 'programmatic_auth_chromeos?hl=' + data.hl +
              '&scope=https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthLogin&' +
              'client_id=' + encodeURIComponent(data.clientId) +
              '&access_type=offline');
      if (data.rart) {
        url = appendParam(url, 'rart', data.rart);
      }
      if (data.autoReloadAttempts) {
        url = appendParam(url, 'auto_reload_attempts', data.autoReloadAttempts);
      }

      return url;
    }

    if (data.chromeType) {
      url = appendParam(url, 'chrometype', data.chromeType);
    }
    if (data.clientId) {
      url = appendParam(url, 'client_id', data.clientId);
    }
    if (data.enterpriseDomainManager) {
      url = appendParam(url, 'devicemanager', data.enterpriseDomainManager);
    }
    if (data.clientVersion) {
      url = appendParam(url, 'client_version', data.clientVersion);
    }
    if (data.platformVersion) {
      url = appendParam(url, 'platform_version', data.platformVersion);
    }
    if (data.releaseChannel) {
      url = appendParam(url, 'release_channel', data.releaseChannel);
    }
    if (data.endpointGen) {
      url = appendParam(url, 'endpoint_gen', data.endpointGen);
    }
    if (data.menuEnterpriseEnrollment) {
      url = appendParam(url, 'mi', 'ee');
    }

    if (data.lsbReleaseBoard) {
      url = appendParam(url, 'chromeos_board', data.lsbReleaseBoard);
    }
    if (data.isFirstUser) {
      url = appendParam(url, 'is_first_user', 'true');
    }
    if (data.obfuscatedOwnerId) {
      url = appendParam(url, 'obfuscated_owner_id', data.obfuscatedOwnerId);
    }
    if (data.hl) {
      url = appendParam(url, 'hl', data.hl);
    }
    if (data.gaiaId) {
      url = appendParam(url, 'user_id', data.gaiaId);
    }
    if (data.email) {
      if (data.readOnlyEmail) {
        url = appendParam(url, 'Email', data.email);
      } else {
        url = appendParam(url, 'email_hint', data.email);
      }
    }
    if (this.isConstrainedWindow_) {
      url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
    }
    if (data.flow) {
      url = appendParam(url, 'flow', data.flow);
    }
    if (data.emailDomain) {
      // Use 'hd' (hosted domain) as the argument to show an email domain.
      url = appendParam(url, 'hd', data.emailDomain);
    }
    if (data.showTos) {
      url = appendParam(url, 'show_tos', data.showTos);
    }
    if (data.ignoreCrOSIdpSetting === true) {
      url = appendParam(url, 'ignoreCrOSIdpSetting', 'true');
    }
    if (data.enableGaiaActionButtons) {
      url = appendParam(url, 'use_native_navigation', '1');
    }
    if (data.isSupervisedUser) {
      url = appendParam(url, 'is_supervised', '1');
    }
    if (data.isDeviceOwner) {
      url = appendParam(url, 'is_device_owner', '1');
    }
    if (data.rart) {
      url = appendParam(url, 'rart', data.rart);
    }
    if (data.forceDarkMode) {
      url = appendParam(url, 'color_scheme', 'dark');
    }
    if (data.pwl) {
      url = appendParam(url, 'pwl', data.pwl);
    }
    if (data.autoReloadAttempts) {
      url = appendParam(url, 'auto_reload_attempts', data.autoReloadAttempts);
    }

    return url;
  }

  /**
   * Dispatches the 'ready' event if it hasn't been dispatched already for the
   * current content.
   * @private
   */
  fireReadyEvent_() {
    if (!this.readyFired_) {
      this.dispatchEvent(new Event('ready'));
      this.readyFired_ = true;
    }
  }

  /**
   * Invoked when a main frame request in the webview has completed.
   * @private
   */
  onRequestCompleted_(details) {
    const currentUrl = details.url;

    if (!currentUrl.startsWith('https')) {
      this.trusted_ = false;
    }

    if (this.isConstrainedWindow_) {
      let isEmbeddedPage = false;
      if (this.idpOrigin_ && currentUrl.startsWith(this.idpOrigin_)) {
        const headers = details.responseHeaders;
        for (let i = 0; headers && i < headers.length; ++i) {
          if (headers[i].name.toLowerCase() === EMBEDDED_FORM_HEADER) {
            isEmbeddedPage = true;
            break;
          }
        }
      }

      // In some cases, non-embedded pages should not be resized.  For
      // example, on desktop when reauthenticating for purposes of unlocking
      // a profile, resizing would cause a browser window to open in the
      // system profile, which is not allowed.
      if (!isEmbeddedPage && !this.dontResizeNonEmbeddedPages) {
        this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
        return;
      }
    }

    this.updateHistoryState_(currentUrl);
  }

  /**
   * Manually updates the history. Invoked upon completion of a webview
   * navigation.
   * @param {string} url Request URL.
   * @private
   */
  updateHistoryState_(url) {
    if (history.state && history.state.url !== url) {
      history.pushState({url: url}, '');
    } else {
      history.replaceState({url: url}, '');
    }
  }

  /**
   * Invoked when the sign-in page takes focus.
   * @private
   */
  onFocus_() {
    if (this.authMode === AuthMode.DESKTOP &&
        document.activeElement === document.body) {
      this.webview_.focus();
    }
  }

  /**
   * Invoked when the history state is changed.
   * @param {!Event} e The popstate event being triggered.
   * @private
   */
  onPopState_(e) {
    const state = e.state;
    if (state && state.url) {
      this.webview_.src = state.url;
    }
  }

  /**
   * Invoked when headers are received in the main frame of the webview. It
   * reads the authenticated user info from a signin header.
   * @param {OnHeadersReceivedDetails} details
   * @private
   */
  onHeadersReceived_(details) {
    if (this.authCompletedFired_) {
      // SIGN_IN_HEADER could be sent more thane once. Sometimes already
      // after authentication completed. Return here to avoid triggering
      // maybeCompleteAuth which shows "create your password screen" because
      // scraped passwords are wiped at that point.
      return;
    }
    const currentUrl = details.url;
    if (this.idpOrigin_ === null || this.idpOrigin_ === undefined ||
      !currentUrl.startsWith(this.idpOrigin_)) {
      return;
    }

    const headers = details.responseHeaders;
    for (let i = 0; headers && i < headers.length; ++i) {
      const header = headers[i];
      const headerName = header.name.toLowerCase();
      if (headerName === SIGN_IN_HEADER) {
        const headerValues = header.value.toLowerCase().split(',');
        const signinDetails = {};
        headerValues.forEach(function(e) {
          const pair = e.split('=');
          signinDetails[pair[0].trim()] = pair[1].trim();
        });
        // Removes "" around.
        const email = signinDetails['email'].slice(1, -1);
        this.setEmail_(email);
        this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
        this.sessionIndex_ = signinDetails['sessionindex'];
      }
    }
  }

  /**
   * Returns true if given HTML5 message is received from `this.idpOrigin_` -
   * which is usually Gaia.
   * @param {Object} e Payload of the received HTML5 message.
   */
  isGaiaMessage_(e) {
    if (!this.isWebviewEvent_(e)) {
      return false;
    }

    // The event origin does not have a trailing slash, while `idpOrigin_` does.
    // Strip the trailing slash from `idpOrigin_` before comparison.
    if (e.origin !== this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) {
      return false;
    }

    // Gaia messages must be an object with 'method' property.
    if (typeof e.data !== 'object' || !e.data.hasOwnProperty('method')) {
      return false;
    }
    return true;
  }

  /**
   * Invoked when an HTML5 message is received from the webview element.
   * @param {Object} e Payload of the received HTML5 message.
   * @private
   */
  onMessageFromWebview_(e) {
    if (!this.isGaiaMessage_(e)) {
      return;
    }

    const msg = e.data;
    if (msg.method in messageHandlers) {
      if (this.authCompletedFired_) {
        console.warn(msg.method + ' message sent after auth completed');
      }
      messageHandlers[msg.method].call(this, msg);
    } else if (!IGNORED_MESSAGES_FROM_GAIA.includes(msg.method)) {
      console.warn('Unrecognized message from GAIA: ' + msg.method);
    }
  }

  /**
   * Invoked to send a HTML5 message with attached data to the webview
   * element.
   * @param {string} messageType Type of the HTML5 message.
   * @param {string|Object=} messageData Data to be attached to the message.
   */
  sendMessageToWebview(messageType, messageData = null) {
    const currentUrl = this.webview_.src;
    let payload = undefined;
    if (messageData) {
      payload = {type: messageType, data: messageData};
    } else {
      // TODO(crbug.com/1116343): Use new message format when it will be
      // available in production.
      payload = messageType;
    }

    this.webview_.contentWindow.postMessage(payload, currentUrl);
  }

  /**
   * Invoked by the hosting page to verify the Saml password.
   */
  verifyConfirmedPassword(password) {
    if (!this.samlHandler_.verifyConfirmedPassword(password)) {
      this.confirmPasswordCallback(
          this.email_, this.samlHandler_.scrapedPasswordCount);
      return;
    }

    this.password_ = password;
    this.onAuthCompleted_();
  }

  /**
   * Check Saml flow and start password confirmation flow if needed.
   * Otherwise, continue with auto completion.
   * @private
   */
  maybeCompleteAuth_() {
    if (this.authCompletedFired_) {
      return;
    }
    const missingGaiaInfo =
        !this.email_ || !this.gaiaId_ || !this.sessionIndex_;
    if (missingGaiaInfo && !this.skipForNow_) {
      if (this.missingGaiaInfoCallback) {
        this.missingGaiaInfoCallback();
      }

      this.webview_.src = this.initialFrameUrl_;
      return;
    }

    // Could be set either by `userInfo` message or by the
    // `onGaiaDoneTimeout_`.
    const userInfoAvailable = !!this.services_;

    const gaiaDone = userInfoAvailable && this.closeViewReceived_ &&
        !this.waitApiPasswordConfirm_;

    if (gaiaDone) {
      this.maybeRecordGaiaElapsedTime_();
      this.maybeRecordAccountFreshnessInOobe_();
      this.maybeClearGaiaTimeout_();
    } else if (this.gaiaDoneTimer_) {
      // Early out if `gaiaDoneTimer_` is running.
      return;
    } else {
      this.gaiaStartTime = Date.now();
      // Start `gaiaDoneTimer_` if Gaia is not yet done.
      this.gaiaDoneTimer_ = window.setTimeout(
          () => this.onGaiaDoneTimeout_(), GAIA_DONE_WAIT_TIMEOUT_MS);
      return;
    }

    if (this.recordSamlProviderCallback && this.authFlow === AuthFlow.SAML) {
      // Makes distinction between different SAML providers
      this.recordSamlProviderCallback(this.samlHandler_.x509certificate || '');
    }

    if (this.samlHandler_.samlApiUsed) {
      if (this.samlApiUsedCallback) {
        // Makes distinction between Gaia and Chrome Credentials Passing API
        // login to properly fill ChromeOS.SAML.ApiLogin metrics.
        this.samlApiUsedCallback(this.authFlow === AuthFlow.SAML);
      }
      this.password_ = this.samlHandler_.apiPasswordBytes;
      this.onAuthCompleted_();
      return;
    }

    if (this.samlHandler_.scrapedPasswordCount === 0) {
      if (this.noPasswordCallback) {
        this.noPasswordCallback(this.email_);
        return;
      }

      // Fall through to finish the auth flow even if this.needPassword
      // is true. This is because the flag is used as an intention to get
      // password when it is available but not a mandatory requirement.
      console.warn('Authenticator: No password scraped for SAML.');
    } else if (this.needPassword) {
      if (this.samlHandler_.scrapedPasswordCount === 1) {
        // If we scraped exactly one password, we complete the
        // authentication right away.
        this.password_ = this.samlHandler_.firstScrapedPassword;
        if (this.onePasswordCallback) {
          this.onePasswordCallback();
        }
        this.onAuthCompleted_();
        return;
      }

      if (this.confirmPasswordCallback) {
        // Confirm scraped password. The flow follows in
        // verifyConfirmedPassword.
        this.confirmPasswordCallback(
            this.email_, this.samlHandler_.scrapedPasswordCount);
        return;
      }
    }

    this.onAuthCompleted_();
  }

  /**
   * Invoked to complete the authentication using the password the user
   * enters manually for SAML IdPs that do not use Chrome Credentials Passing
   * API and we couldn't scrape their password input.
   */
  completeAuthWithManualPassword(password) {
    this.password_ = password;
    this.onAuthCompleted_();
  }

  /**
  /**
   * Asserts the |arr| which is known as |nameOfArr| is an array of strings.
   * @private
   */
  assertStringArray_(arr, nameOfArr) {
    console.assert(
        Array.isArray(arr), 'FATAL: Bad %s type: %s', nameOfArr, typeof arr);
    for (let i = 0; i < arr.length; ++i) {
      this.assertStringElement_(arr[i], nameOfArr, i);
    }
  }

  /**
   * Asserts the |dict| which is known as |nameOfDict| is a dict of strings.
   * @private
   */
  assertStringDict_(dict, nameOfDict) {
    console.assert(
        typeof dict === 'object', 'FATAL: Bad %s type: %s', nameOfDict,
        typeof dict);
    for (const key in dict) {
      this.assertStringElement_(dict[key], nameOfDict, key);
    }
  }

  /** Asserts an element |elem| in a certain collection is a string. */
  assertStringElement_(elem, nameOfCollection, index) {
    console.assert(
        typeof elem === 'string', 'FATAL: Bad %s[%s] type: %s',
        nameOfCollection, index, typeof elem);
  }

  /**
   * Invoked to process authentication completion.
   * @private
   */
  onAuthCompleted_() {
    assert(
        this.skipForNow_ ||
        (this.email_ && this.gaiaId_ && this.sessionIndex_));
    let scrapedPasswords = [];
    if (this.authFlow === AuthFlow.SAML && !this.samlHandler_.samlApiUsed) {
      scrapedPasswords = this.samlHandler_.scrapedPasswords;
    }
    // Chrome will crash on incorrect data type, so log some error message
    // here.
    if (this.services_) {
      this.assertStringArray_(this.services_, 'services');
    }
    let passwordAttributes = {};
    if (this.authFlow === AuthFlow.SAML &&
        this.samlHandler_.extractSamlPasswordAttributes) {
      passwordAttributes = this.samlHandler_.passwordAttributes;
    }
    this.assertStringDict_(passwordAttributes, 'passwordAttributes');
    this.dispatchEvent(new CustomEvent(
        'authCompleted',
        // TODO(rsorokin): get rid of the stub values.
        {
          detail: {
            email: this.email_ || '',
            gaiaId: this.gaiaId_ || '',
            password: this.password_ || '',
            usingSAML: this.authFlow === AuthFlow.SAML,
            scrapedSAMLPasswords: scrapedPasswords,
            publicSAML: this.samlAclUrl_ || false,
            skipForNow: this.skipForNow_,
            sessionIndex: this.sessionIndex_ || '',
            trusted: this.trusted_,
            services: this.services_ || [],
            servicesProvided: this.servicesProvided_,
            passwordAttributes: passwordAttributes,
            syncTrustedVaultKeys: this.syncTrustedVaultKeys_ || {},
          },
        }));
    this.resetStates();
    this.authCompletedFired_ = true;
  }

  /**
   * Invoked when |samlHandler_| fires 'insecureContentBlocked' event.
   * @private
   */
  onInsecureContentBlocked_(e) {
    if (!this.isLoaded_) {
      return;
    }

    if (this.insecureContentBlockedCallback) {
      this.insecureContentBlockedCallback(e.detail.url);
    } else {
      console.error('Authenticator: Insecure content blocked.');
    }
  }

  /**
   * Invoked when |samlHandler_| fires 'authPageLoaded' event.
   * @private
   */
  onAuthPageLoaded_(e) {
    if (!this.isLoaded_) {
      return;
    }

    if (!e.detail.isSAMLPage) {
      return;
    }

    this.authFlow = AuthFlow.SAML;

    this.webview_.focus();
    this.fireReadyEvent_();
  }

  /**
   * Invoked when |samlHandler_| fires 'apiPasswordAdded' event. Could be from
   * 3rd-party SAML IdP or Gaia which also uses the API.
   * @private
   */
  onSamlApiPasswordAdded_(e) {
    this.dispatchEvent(new Event('apiPasswordAdded'));
    this.waitApiPasswordConfirm_ = true;

    // Saml API 'add' password might be received after the 'loadcommit'
    // event. In such case, maybeCompleteAuth_ should be attempted again if
    // GAIA ID is available.
    if (this.gaiaId_) {
      this.maybeCompleteAuth_();
    }
  }

  /**
   * Invoked when |samlHandler_| fires 'apiAccountCreated' event.
   * @private
   */
  onSamlApiAccountCreated_(e) {
    this.isNewAccount = true;
    this.recordAccountCreated_();
  }

  /**
   * Invoked when |samlHandler_| fires 'apiPasswordConfirmed' event. Could be
   * from 3rd-party SAML IdP or Gaia which also uses the API.
   * @private
   */
  onSamlApiPasswordConfirmed_(e) {
    this.waitApiPasswordConfirm_ = false;
    if (this.gaiaId_) {
      this.maybeCompleteAuth_();
    }
  }

  /**
   * Invoked when |samlHandler_| fires 'challengeMachineKeyRequired' event.
   * @private
   */
  onChallengeMachineKeyRequired_(e) {
    sendWithPromise('samlChallengeMachineKey', e.detail.url, e.detail.challenge)
        .then(e.detail.callback);
  }

  /**
   * Invoked when a link is dropped on the webview.
   * @private
   */
  onDropLink_(e) {
    this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
  }

  /**
   * Invoked when the webview attempts to open a new window.
   * @private
   */
  onNewWindow_(e) {
    this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
  }

  /**
   * Invoked when a new document is loaded.
   * @private
   */
  onContentLoad_(e) {
    if (this.isConstrainedWindow_) {
      // Signin content in constrained windows should not zoom. Isolate the
      // webview from the zooming of other webviews using the 'per-view'
      // zoom mode, and then set it to 100% zoom.
      this.webview_.setZoomMode('per-view');
      this.webview_.setZoom(1);
    }

    // Posts a message to IdP pages to initiate communication.
    const currentUrl = this.webview_.src;
    if (this.idpOrigin_ && currentUrl.startsWith(this.idpOrigin_)) {
      const msg = {
        'method': 'handshake',
      };

      // |this.webview_.contentWindow| may be null after network error
      // screen is shown. See crbug.com/770999.
      if (this.webview_.contentWindow) {
        this.webview_.contentWindow.postMessage(msg, currentUrl);
      } else {
        console.error('Authenticator: contentWindow is null.');
      }

      this.fireReadyEvent_();
      // Focus webview after dispatching event when webview is already
      // visible.
      this.webview_.focus();
    } else if (currentUrl === BLANK_PAGE_URL) {
      this.fireReadyEvent_();
    } else if (currentUrl === this.samlAclUrl_) {
      this.skipForNow_ = true;
      this.onAuthCompleted_();
    }
  }

  /**
   * Invoked when the webview fails loading a page.
   * @private
   */
  onLoadAbort_(e) {
    if (this.samlHandler_.isIntentionalAbort()) {
      return;
    }

    // Ignore errors from subframe loads, as these should not cause an error
    // screen to be displayed. When a subframe load is triggered, it means
    // that the main frame load has succeeded, so the host is reachable in
    // general.
    if (!e.isTopLevel) {
      return;
    }

    this.dispatchEvent(new CustomEvent(
        'loadAbort', {detail: {error_code: e.code, src: e.url}}));
  }

  /**
   * Invoked when the webview navigates withing the current document.
   * @private
   */
  onLoadCommit_(e) {
    if (this.gaiaId_) {
      this.maybeCompleteAuth_();
    }
    if (e.isTopLevel) {
      this.authDomain = extractDomain(e.url);
    }
  }

  /**
   * Returns |true| if event |e| was sent from the hosted webview.
   * @private
   */
  isWebviewEvent_(e) {
    const webviewWindow = this.webview_.contentWindow;
    return !!webviewWindow && webviewWindow === e.source;
  }

  /**
   * Callback for the user info message waiting timeout.
   * @private
   */
  onGaiaDoneTimeout_() {
    if (!this.services_) {
      console.warn('Gaia done timeout: Forcing empty services.');
      this.services_ = [];
      const metric = this.authFlow === AuthFlow.SAML ?
          GAIA_MESSAGE_SAML_USER_INFO :
          GAIA_MESSAGE_GAIA_USER_INFO;
      chrome.send('metricsHandler:recordBooleanHistogram', [metric, false]);
    }

    if (!this.closeViewReceived_) {
      console.warn('Gaia done timeout: closeView was not called.');
      this.closeViewReceived_ = true;

      const metric = this.authFlow === AuthFlow.SAML ?
          GAIA_MESSAGE_SAML_CLOSE_VIEW :
          GAIA_MESSAGE_GAIA_CLOSE_VIEW;
      chrome.send('metricsHandler:recordBooleanHistogram', [metric, false]);
    }

    if (this.waitApiPasswordConfirm_) {
      // Log duplicates the log from the saml handler. The message is used by
      // the tast test to catch failures.
      console.warn('SamlHandler.onAPICall_: API password was not confirmed');
      this.samlHandler_.recordPasswordNotConfirmedError();
      this.waitApiPasswordConfirm_ = false;
    }

    this.maybeClearGaiaTimeout_();
    this.maybeCompleteAuth_();
  }

  /**
   * @private
   */
  maybeRecordGaiaElapsedTime_() {
    if (!this.gaiaStartTime) {
      return;
    }
    chrome.send('metricsHandler:recordTime', [
      GAIA_DONE_ELAPSED_TIME,
      Date.now() - this.gaiaStartTime,
    ]);
    this.gaiaStartTime = null;
  }

  /**
   * Record if the sign-in account in Oobe is an existing account or new
   * account.
   * @private
   */
  maybeRecordAccountFreshnessInOobe_() {
      // Record the metric if the record new account feature
      // flag is enabled. This metric is recorded only for the sign-in
      // event happens in Oobe.
      if (!this.samlHandler_.shouldHandleAccountCreationMessage ||
          !this.isFirstUser_) {
        return;
      }
      chrome.send('metricsHandler:recordBooleanHistogram', [
        GAIA_DONE_OOBE_NEW_ACCOUNT,
        this.isNewAccount
      ]);
      this.isNewAccount = false;
    }

  /**
   * Record new account creation.
   * @private
   */
  recordAccountCreated_() {
    // Record true account is created during the first sign in event
    // and false if another account existed.
    // TODO (b/307591058): add metric to track if account is created
    // during login or not.
    chrome.send('metricsHandler:recordBooleanHistogram',[
      GAIA_CREATE_ACCOUNT_FIRST_USER,
      this.isFirstUser_
    ]);
  }

  /**
   * @private
   */
  maybeClearGaiaTimeout_() {
    if (!this.gaiaDoneTimer_) {
      return;
    }
    window.clearTimeout(this.gaiaDoneTimer_);
    this.gaiaDoneTimer_ = null;
  }

  /**
   * Disables all navigation actions until explicitly re-enabled by GAIA.
   * @private
   */
  disableAllActions_() {
    this.dispatchEvent(
        new CustomEvent('setAllActionsEnabled', {detail: false}));
  }

  /**
   * Set the user's email.
   * @param {string} email New email value.
   * @private
   */
  setEmail_(email) {
    this.email_ = email;
    this.samlHandler_.email = email;
  }
}