Home Reference Source

src/salte-auth.js

import assign from 'lodash/assign';
import defaultsDeep from 'lodash/defaultsDeep';
import get from 'lodash/get';
import set from 'lodash/set';
import uuid from 'uuid';
import debug from 'debug';

import { Providers } from './salte-auth.providers.js';
import { SalteAuthProfile } from './salte-auth.profile.js';
import { SalteAuthUtilities } from './salte-auth.utilities.js';
import { SalteAuthMixinGenerator } from './salte-auth.mixin.js';

/** @ignore */
const logger = debug('@salte-auth/salte-auth');

/**
 * Disable certain security validations if your provider doesn't support them.
 * @typedef {Object} Validation
 * @property {Boolean} [nonce=true] Passing false will disable nonce validation, leaving you vulnerable to replay attacks.
 * @property {Boolean} [state=true] Passing false will disable state validation, leaving you vulnerable to XSRF attacks.
 * @property {Boolean} [azp=true] Passing false will disable azp validation.
 * @property {Boolean} [aud=true] Passing false will disable aud validation.
 */

/**
 * Disable certain security validations if your provider doesn't support them.
 * @typedef {Object} RedirectURLs
 * @property {String} [loginUrl] The redirect url specified in your identity provider for logging in.
 * @property {String} [logoutUrl] The redirect url specified in your identity provider for logging out.
 */

/**
 * The configuration for salte auth
 * @typedef {Object} Config
 * @property {String} providerUrl The base url of your identity provider.
 * @property {('id_token'|'id_token token'|'code')} responseType The response type to authenticate with.
 * @property {String|RedirectURLs} redirectUrl The redirect url specified in your identity provider.
 * @property {String} clientId The client id of your identity provider
 * @property {String} scope A list of space-delimited claims used to determine what user information is provided and what access is given. Most providers require 'openid'.
 * @property {Boolean|Array<String>} routes A list of secured routes. If true is provided then all routes are secured.
 * @property {Array<String|RegExp>} endpoints A list of secured endpoints.
 * @property {('auth0'|'azure'|'cognito'|'wso2'|'okta')} provider The identity provider you're using.
 * @property {('iframe'|'redirect'|false)} [loginType='iframe'] The automated login type to use.
 * @property {Function} [redirectLoginCallback] A callback that is invoked when a redirect login fails or succeeds.
 * @property {('session'|'local')} [storageType='session'] The Storage api to keep authenticate information stored in.
 * @property {Boolean|Validation} [validation] Used to disable certain security validations if your provider doesn't support them.
 * @property {Boolean} [autoRefresh=true] Automatically refreshes the users token upon switching tabs or one minute prior to expiration.
 * @property {Number} [autoRefreshBuffer=60000] A number of miliseconds before token expiration to refresh.
 * @property {Object} [queryParams] A key-value set of additional query params to attached to the login request.
 */

/**
 * The configuration for salte auth
 * @typedef {Object} LoginConfig
 * @property {Boolean} [noPrompt=false] Disables login prompts, this should only be used for token renewal!
 * @property {(false|'errors'|'all')} [clear='all'] Whether to clear "all" profile information, only "errors", or nothing.
 * @property {Boolean} [events=true] Whether events should be fired off if the login is successful or not.
 */

/**
 * Authentication Controller
 */
class SalteAuth {
  /**
   * Sets up Salte Auth
   * @param {Config} config configuration for salte auth
   */
  constructor(config) {
    if (window.salte.auth) {
      return window.salte.auth;
    }

    if (!config) {
      throw new ReferenceError('A config must be provided.');
    }

    /**
     * The supported identity providers
     * @type {Providers}
     * @private
     */
    this.$providers = Providers;
    /**
     * The active authentication promises
     * @private
     */
    this.$promises = {};
    /**
     * The active authentication timeouts
     * @private
     */
    this.$timeouts = {};
    /**
     * The registered listeners
     * @private
     */
    this.$listeners = {};
    /**
     * The configuration for salte auth
     * @type {Config}
     * @private
     */
    this.$config = config;
    this.$config = defaultsDeep(config, this.$provider.defaultConfig, {
      loginType: 'iframe',
      autoRefresh: true,
      autoRefreshBuffer: 60000
    });
    /**
     * Various utility functions for salte auth
     * @type {SalteAuthUtilities}
     * @private
     */
    this.$utilities = new SalteAuthUtilities(this.$config);

    /**
     * The user profile for salte auth
     * @type {SalteAuthProfile}
     */
    this.profile = new SalteAuthProfile(this.$config);

    /**
     * A mixin built for Web Components
     *
     * @example
     * class MyElement extends auth.mixin(HTMLElement) {
     *   constructor() {
     *     super();
     *
     *     console.log(this.auth); // This is the same as auth
     *     console.log(this.user); // This is the same as auth.profile.userInfo.
     *     console.log(this.authenticated); // This is the same as auth.profile.idTokenExpired.
     *   }
     * }
     */
    this.mixin = SalteAuthMixinGenerator(this);

    if (this.$utilities.$iframe) {
      logger('Detected iframe, removing...');
      this.profile.$parseParams();
      parent.document.body.removeChild(this.$utilities.$iframe);
    } else if (this.$utilities.$popup) {
      logger('Popup detected!');
    } else if (this.profile.$redirectUrl && location.href !== this.profile.$redirectUrl) {
      logger('Redirect detected!');
      this.profile.$parseParams();
      const error = this.profile.$validate();

      // Delay for an event loop to give users time to register a listener.
      setTimeout(() => {
        const action = this.profile.$actions(this.profile.$state);

        if (error) {
          this.profile.$clear();
        } else {
          logger(`Navigating to Redirect URL... (${this.profile.$redirectUrl})`);
          this.$utilities.$navigate(this.profile.$redirectUrl);
          this.profile.$redirectUrl = undefined;
        }

        if (action === 'login') {
          this.$fire('login', error || null, this.profile.code || this.profile.userInfo);
        } else if (action === 'logout') {
          this.$fire('logout', error);
        }

        // TODO(v3.0.0): Remove the `redirectLoginCallback` api from `salte-auth`.
        this.$config.redirectLoginCallback && this.$config.redirectLoginCallback(error);
      });
    } else {
      logger('Setting up interceptors...');
      this.$utilities.addXHRInterceptor((request, data) => {
        if (this.$config.responseType !== 'code' && this.$utilities.checkForMatchingUrl(request.$url, this.$config.endpoints)) {
          return this.retrieveAccessToken().then((accessToken) => {
            request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
          });
        }
      });

      this.$utilities.addFetchInterceptor((request) => {
        if (this.$config.responseType !== 'code' && this.$utilities.checkForMatchingUrl(request.url, this.$config.endpoints)) {
          return this.retrieveAccessToken().then((accessToken) => {
            request.headers.set('Authorization', `Bearer ${accessToken}`);
          });
        }
      });

      logger('Setting up route change detectors...');
      window.addEventListener('popstate', this.$$onRouteChanged.bind(this), { passive: true });
      document.addEventListener('click', this.$$onRouteChanged.bind(this), { passive: true });
      setTimeout(this.$$onRouteChanged.bind(this));

      logger('Setting up automatic renewal of token...');
      this.on('login', (error) => {
        if (error) return;

        this.$$refreshToken();
      });

      this.on('refresh', (error) => {
        if (error) return;

        this.$$refreshToken();
      });

      this.on('logout', () => {
        clearTimeout(this.$timeouts.refresh);
      });

      if (!this.profile.idTokenExpired) {
        this.$$refreshToken();
      }

      document.addEventListener('visibilitychange', this.$$onVisibilityChanged.bind(this), {
        passive: true
      });

      this.$fire('create', null, this);
    }

    // TODO(v3.0.0): Revoke singleton status from `salte-auth`.
    window.salte.auth = this;

    if (this.$config.redirectLoginCallback) {
      console.warn(`The "redirectLoginCallback" api has been deprecated in favor of the "on" api, see http://bit.ly/salte-auth-on for more info.`);
    }
  }

  /**
   * Returns the configured provider
   * @type {Class|Object}
   * @private
   */
  get $provider() {
    if (!this.$config.provider) {
      throw new ReferenceError('A provider must be specified');
    }

    if (typeof this.$config.provider === 'string') {
      const provider = this.$providers[this.$config.provider];
      if (!provider) {
        throw new ReferenceError(`Unknown Provider (${this.$config.provider})`);
      }
      return provider;
    }

    return this.$config.provider;
  }

  /**
   * The authentication url to retrieve the access token
   * @type {String}
   * @private
   */
  get $accessTokenUrl() {
    this.profile.$localState = uuid.v4();
    this.profile.$nonce = uuid.v4();

    let authorizeEndpoint = `${this.$config.providerUrl}/authorize`;
    if (this.$provider.authorizeEndpoint) {
      authorizeEndpoint = this.$provider.authorizeEndpoint.call(this, this.$config);
    }

    return this.$utilities.createUrl(authorizeEndpoint, assign({
      'state': this.profile.$localState,
      'nonce': this.profile.$nonce,
      'response_type': 'token',
      'redirect_uri': this.$config.redirectUrl && this.$config.redirectUrl.loginUrl || this.$config.redirectUrl,
      'client_id': this.$config.clientId,
      'scope': this.$config.scope,
      'prompt': 'none'
    }, this.$config.queryParams));
  }

  /**
   * The authentication url to retrieve the id token
   * @param {Boolean} refresh Whether this request is intended to refresh the token.
   * @return {String} the computed login url
   * @private
   */
  $loginUrl(refresh) {
    this.profile.$localState = uuid.v4();
    this.profile.$nonce = uuid.v4();

    let authorizeEndpoint = `${this.$config.providerUrl}/authorize`;
    if (this.$provider.authorizeEndpoint) {
      authorizeEndpoint = this.$provider.authorizeEndpoint.call(this, this.$config);
    }

    return this.$utilities.createUrl(authorizeEndpoint, assign({
      'state': this.profile.$localState,
      'nonce': this.profile.$nonce,
      'response_type': this.$config.responseType,
      'redirect_uri': this.$config.redirectUrl && this.$config.redirectUrl.loginUrl || this.$config.redirectUrl,
      'client_id': this.$config.clientId,
      'scope': this.$config.scope,
      'prompt': refresh ? 'none' : undefined
    }, this.$config.queryParams));
  }

  /**
   * The url to logout of the configured provider
   * @type {String}
   * @private
   */
  get $deauthorizeUrl() {
    return this.$provider.deauthorizeUrl.call(this, defaultsDeep(this.$config, {
      idToken: this.profile.$idToken
    }));
  }

  /**
   * Listens for an event to be invoked.
   * @param {('login'|'logout'|'refresh'|'expired')} eventType the event to listen for.
   * @param {Function} callback A callback that fires when the specified event occurs.
   *
   * @example
   * auth.on('login', (error, user) => {
   *   if (error) {
   *     console.log('something bad happened!');
   *   }
   *
   *   console.log(user); // This is the same as auth.profile.userInfo.
   * });
   *
   * @example
   * window.addEventListener('salte-auth-login', (event) => {
   *   if (event.detail.error) {
   *     console.log('something bad happened!');
   *   }
   *
   *   console.log(event.detail.data); // This is the same as auth.profile.userInfo.
   * });
   */
  on(eventType, callback) {
    if (['login', 'logout', 'refresh', 'expired'].indexOf(eventType) === -1) {
      throw new ReferenceError(`Unknown Event Type (${eventType})`);
    } else if (typeof callback !== 'function') {
      throw new ReferenceError('Invalid callback provided!');
    }

    this.$listeners[eventType] = this.$listeners[eventType] || [];
    this.$listeners[eventType].push(callback);
  }

  /**
   * Deregister a callback previously registered.
   * @param {('login'|'logout'|'refresh'|'expired')} eventType the event to deregister.
   * @param {Function} callback A callback that fires when the specified event occurs.
   *
   * @example
   * const someFunction = function() {};
   *
   * auth.on('login', someFunction);
   *
   * auth.off('login', someFunction);
   */
  off(eventType, callback) {
    if (['login', 'logout', 'refresh', 'expired'].indexOf(eventType) === -1) {
      throw new ReferenceError(`Unknown Event Type (${eventType})`);
    } else if (typeof callback !== 'function') {
      throw new ReferenceError('Invalid callback provided!');
    }

    const eventListeners = this.$listeners[eventType];
    if (!eventListeners || !eventListeners.length) return;

    const index = eventListeners.indexOf(callback);
    eventListeners.splice(index, 1);
  }

  /**
   * Fires off an event to a given set of listeners
   * @param {String} eventType The event that occurred.
   * @param {Error} error The error tied to this event.
   * @param {*} data The data tied to this event.
   * @private
   */
  $fire(eventType, error, data) {
    const event = document.createEvent('Event');
    event.initEvent(`salte-auth-${eventType}`, false, true);
    event.detail = { error, data };
    window.dispatchEvent(event);

    const eventListeners = this.$listeners[eventType];

    if (!eventListeners || !eventListeners.length) return;

    eventListeners.forEach((listener) => listener(error, data));
  }

  /**
   * Authenticates using the iframe-based OAuth flow.
   * @param {Boolean|LoginConfig} config Whether this request is intended to refresh the token.
   * @return {Promise<Object>} a promise that resolves when we finish authenticating
   *
   * @example
   * auth.loginWithIframe().then((user) => {
   *   console.log(user); // This is the same as auth.profile.userInfo.
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  loginWithIframe(config) {
    if (this.$promises.login) {
      return this.$promises.login;
    }

    // TODO(v3.0.0): Remove backwards compatibility with refresh boolean.
    if (typeof config === 'boolean') {
      config = {
        noPrompt: config,
        clear: config ? 'errors' : undefined,
        events: false,
        timeout: 3000
      };
    }

    config = defaultsDeep(config, {
      noPrompt: false,
      clear: 'all',
      events: true
    });

    if (config.clear === 'all') {
      this.profile.$clear();
    } else if (config.clear === 'errors') {
      this.profile.$clearErrors();
    }

    this.$promises.login = this.$utilities.createIframe(this.$loginUrl(config.noPrompt), !config.noPrompt, config.timeout).then(() => {
      this.$promises.login = null;
      const error = this.profile.$validate();

      if (error) {
        return Promise.reject(error);
      }

      const response = this.profile.code || this.profile.userInfo;
      if (config.events) {
        this.$fire('login', null, response);
      }
      return response;
    }).catch((error) => {
      this.$promises.login = null;
      if (config.events) {
        this.$fire('login', error);
      }
      return Promise.reject(error);
    });

    return this.$promises.login;
  }

  /**
   * Authenticates using the popup-based OAuth flow.
   * @return {Promise<Object>} a promise that resolves when we finish authenticating
   *
   * @example
   * auth.loginWithPopup().then((user) => {
   *   console.log(user); // This is the same as auth.profile.userInfo.
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  loginWithPopup() {
    if (this.$promises.login) {
      return this.$promises.login;
    }

    this.profile.$clear();
    this.$promises.login = this.$utilities.openPopup(this.$loginUrl()).then(() => {
      this.$promises.login = null;
      this.profile.$parseParams();
      const error = this.profile.$validate();

      if (error) {
        this.profile.$clear();
        return Promise.reject(error);
      }

      const response = this.profile.code || this.profile.userInfo;
      this.$fire('login', null, response);
      return response;
    }).catch((error) => {
      this.$promises.login = null;
      this.$fire('login', error);
      return Promise.reject(error);
    });

    return this.$promises.login;
  }

  /**
   * Authenticates using the tab-based OAuth flow.
   * @return {Promise<Object>} a promise that resolves when we finish authenticating
   *
   * @example
   * auth.loginWithNewTab().then((user) => {
   *   console.log(user); // This is the same as auth.profile.userInfo.
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  loginWithNewTab() {
    if (this.$promises.login) {
      return this.$promises.login;
    }

    this.profile.$clear();
    this.$promises.login = this.$utilities.openNewTab(this.$loginUrl()).then(() => {
      this.$promises.login = null;
      this.profile.$parseParams();
      const error = this.profile.$validate();

      if (error) {
        this.profile.$clear();
        return Promise.reject(error);
      }

      const response = this.profile.code || this.profile.userInfo;
      this.$fire('login', null, response);
      return response;
    }).catch((error) => {
      this.$promises.login = null;
      this.$fire('login', error);
      return Promise.reject(error);
    });

    return this.$promises.login;
  }

  /**
   * Authenticates using the redirect-based OAuth flow.
   * @param {String} redirectUrl override for the redirect url, by default this will try to redirect the user back where they started.
   * @return {Promise} a promise intended to block future login attempts.
   *
   * @example
   * auth.loginWithRedirect(); // Don't bother with utilizing the promise here, it never resolves.
   */
  loginWithRedirect(redirectUrl) {
    if (this.$config.redirectLoginCallback) {
      console.warn(`The "redirectLoginCallback" api has been deprecated in favor of the "on" api, see http://bit.ly/salte-auth-on for more info.`);
    }

    if (this.$promises.login) {
      return this.$promises.login;
    }

    // NOTE: This prevents the other login types from racing "loginWithRedirect".
    // Without this someone could potentially call login somewhere else before
    // the app has a change to redirect. Which could result in an invalid state.
    this.$promises.login = new Promise(() => {});

    this.profile.$clear();
    this.profile.$redirectUrl = redirectUrl && this.$utilities.resolveUrl(redirectUrl) || this.profile.$redirectUrl || location.href;
    const url = this.$loginUrl();

    this.profile.$actions(this.profile.$localState, 'login');
    this.$utilities.$navigate(url);

    return this.$promises.login;
  }

  /**
   * Unauthenticates using the iframe-based OAuth flow.
   * @return {Promise} a promise that resolves when we finish deauthenticating
   *
   * @example
   * auth.logoutWithIframe().then(() => {
   *   console.log('success!');
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  logoutWithIframe() {
    if (this.$promises.logout) {
      return this.$promises.logout;
    }

    const deauthorizeUrl = this.$deauthorizeUrl;
    this.profile.$clear();

    this.$promises.logout = this.$utilities.createIframe(deauthorizeUrl).then(() => {
      this.$promises.logout = null;
      this.$fire('logout');
    }).catch((error) => {
      this.$promises.logout = null;
      this.$fire('logout', error);
      return Promise.reject(error);
    });
    return this.$promises.logout;
  }

  /**
   * Unauthenticates using the popup-based OAuth flow.
   * @return {Promise} a promise that resolves when we finish deauthenticating
   *
   * @example
   * auth.logoutWithPopup().then(() => {
   *   console.log('success!');
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  logoutWithPopup() {
    if (this.$promises.logout) {
      return this.$promises.logout;
    }

    const deauthorizeUrl = this.$deauthorizeUrl;
    this.profile.$clear();

    this.$promises.logout = this.$utilities.openPopup(deauthorizeUrl).then(() => {
      this.$promises.logout = null;
      this.$fire('logout');
    }).catch((error) => {
      this.$promises.logout = null;
      this.$fire('logout', error);
      return Promise.reject(error);
    });

    return this.$promises.logout;
  }

  /**
   * Unauthenticates using the tab-based OAuth flow.
   * @return {Promise} a promise that resolves when we finish deauthenticating
   *
   * @example
   * auth.logoutWithNewTab().then(() => {
   *   console.log('success!');
   * }).catch((error) => {
   *   console.error('Whoops something went wrong!', error);
   * });
   */
  logoutWithNewTab() {
    if (this.$promises.logout) {
      return this.$promises.logout;
    }

    const deauthorizeUrl = this.$deauthorizeUrl;
    this.profile.$clear();

    this.$promises.logout = this.$utilities.openNewTab(deauthorizeUrl).then(() => {
      this.$promises.logout = null;
      this.$fire('logout');
    }).catch((error) => {
      this.$promises.logout = null;
      this.$fire('logout', error);
      return Promise.reject(error);
    });

    return this.$promises.logout;
  }

  /**
   * Logs the user out of their configured identity provider.
   *
   * @example
   * auth.logoutWithRedirect();
   */
  logoutWithRedirect() {
    const deauthorizeUrl = this.$deauthorizeUrl;
    this.profile.$clear();

    this.profile.$actions(this.profile.$localState, 'logout');
    this.$utilities.$navigate(deauthorizeUrl);
  }

  /**
   * Refreshes the users tokens and renews their session.
   * @return {Promise} a promise that resolves when we finish renewing the users tokens.
   */
  refreshToken() {
    if (this.$promises.refresh) {
      return this.$promises.refresh;
    }

    this.$promises.refresh = this.loginWithIframe(true).then((user) => {
      this.$promises.refresh = null;
      const error = this.profile.$validate(true);

      if (error) {
        return Promise.reject(error);
      }
      this.$promises.refresh = null;
      this.$fire('refresh', null, user);
      return user;
    }).catch((error) => {
      this.$promises.refresh = null;
      this.$fire('refresh', error);
      return Promise.reject(error);
    });

    return this.$promises.refresh;
  }
  /**
   * Registers a timeout that will automatically refresh the id token
   */
  $$refreshToken() {
    if (this.$timeouts.refresh !== undefined) {
      clearTimeout(this.$timeouts.refresh);
    }

    if (this.$timeouts.expired !== undefined) {
      clearTimeout(this.$timeouts.expired);
    }

    const timeToExpiration = (this.profile.userInfo.exp * 1000) - Date.now();

    this.$timeouts.refresh = setTimeout(() => {
      // Allows Auto Refresh to be disabled
      if (this.$config.autoRefresh) {
        this.refreshToken().catch((error) => {
          console.error(error);
        });
      } else {
        this.$fire('refresh');
      }
    }, Math.max(timeToExpiration - this.$config.autoRefreshBuffer, 0));

    this.$timeouts.expired = setTimeout(() => {
      this.$fire('expired');
    }, Math.max(timeToExpiration, 0));
  }

  /**
   * Authenticates, requests the access token, and returns it if necessary.
   * @return {Promise<string>} a promise that resolves when we retrieve the access token
   */
  retrieveAccessToken() {
    if (this.$promises.token) {
      logger('Existing token request detected, resolving...');
      return this.$promises.token;
    }

    this.$promises.token = Promise.resolve();
    if ((this.$config.responseType === 'code' && !this.profile.code) || (this.$config.responseType !== 'code' && this.profile.idTokenExpired)) {
      logger('id token has expired, reauthenticating...');
      if (this.$config.loginType === 'iframe') {
        logger('Initiating the iframe flow...');
        this.$promises.token = this.loginWithIframe();
      } else if (this.$config.loginType === 'redirect') {
        this.$promises.token = this.loginWithRedirect();
      } else if (this.$config.loginType === false) {
        if (this.$promises.login) {
          this.$promises.token = this.$promises.login;
        } else {
          this.$promises.token = null;
          return Promise.reject(new ReferenceError('Automatic login is disabled, please login before making any requests!'));
        }
      } else {
        this.$promises.token = null;
        return Promise.reject(new ReferenceError(`Invalid Login Type (${this.$config.loginType})`));
      }
    }

    if (this.$config.responseType !== 'code') {
      this.$promises.token = this.$promises.token.then(() => {
        this.profile.$clearErrors();
        if (this.profile.accessTokenExpired) {
          logger('Access token has expired, renewing...');
          return this.$utilities.createIframe(this.$accessTokenUrl).then(() => {
            const error = this.profile.$validate(true);

            if (error) {
              return Promise.reject(error);
            }
            return this.profile.$accessToken;
          });
        }
        return this.profile.$accessToken;
      });
    }

    if (this.$promises.token) {
      this.$promises.token = this.$promises.token.then((response) => {
        this.$promises.token = null;
        return response;
      }).catch((error) => {
        this.$promises.token = null;
        return Promise.reject(error);
      });
    }

    return this.$promises.token;
  }

  /**
   * Checks if the current route is secured and authenticates the user if necessary
   * @ignore
   */
  $$onRouteChanged() {
    logger('Route change detected, determining if the route is secured...');
    if (!this.$utilities.isRouteSecure(location.href, this.$config.routes)) return;

    logger('Route is secure, verifying tokens...');
    this.retrieveAccessToken();
  }

  /**
   * Disables automatic refresh of the token if the page is no longer visible
   * @ignore
   */
  $$onVisibilityChanged() {
    logger('Visibility change detected, deferring to the next event loop...');
    logger('Determining if the id token has expired...');
    if (this.profile.idTokenExpired || !this.$config.autoRefresh) return;

    if (this.$utilities.$hidden) {
      logger('Page is hidden, refreshing the token...');
      this.refreshToken().then(() => {
        logger('Disabling automatic renewal of the token...');
        clearTimeout(this.$timeouts.refresh);
        this.$timeouts.refresh = null;
      });
    } else {
      logger('Page is visible restarting automatic token renewal...');
      this.$$refreshToken();
    }
  }
}

set(window, 'salte.SalteAuth', get(window, 'salte.SalteAuth', SalteAuth));
export { SalteAuth };
export default SalteAuth;