bcgov/common-forms-toolkit

View on GitHub
app/frontend/src/plugins/keycloak.js

Summary

Maintainability
C
1 day
Test Coverage
// Sourced from https://github.com/fredicious/vue-keycloak-js
import Keycloak from 'keycloak-js';

let installed = false;

export default {
  install: function (Vue, params = {}) {
    if (installed) return;
    installed = true;

    const defaultParams = {
      config: window.__BASEURL__ ? `${window.__BASEURL__}/config` : '/config',
      init: { onLoad: 'login-required' }
    };
    const options = Object.assign({}, defaultParams, params);
    if (assertOptions(options).hasError) throw new Error(`Invalid options given: ${assertOptions(options).error}`);

    const watch = new Vue({
      data() {
        return {
          ready: false,
          authenticated: false,
          userName: null,
          fullName: null,
          token: null,
          tokenParsed: null,
          logoutFn: null,
          loginFn: null,
          login: null,
          createLoginUrl: null,
          createLogoutUrl: null,
          createRegisterUrl: null,
          register: null,
          accountManagement: null,
          createAccountUrl: null,
          loadUserProfile: null,
          loadUserInfo: null,
          subject: null,
          idToken: null,
          idTokenParsed: null,
          realmAccess: null,
          resourceAccess: null,
          refreshToken: null,
          refreshTokenParsed: null,
          timeSkew: null,
          responseMode: null,
          responseType: null,
          hasRealmRole: null,
          hasResourceRole: null
        };
      }
    });
    Object.defineProperty(Vue.prototype, '$keycloak', {
      get() {
        return watch;
      }
    });
    getConfig(options.config)
      .then(config => {
        init(config, watch, options);
      })
      .catch(err => {
        console.log(err); // eslint-disable-line no-console
      });
  }
};

function init(config, watch, options) {
  const ctor = sanitizeConfig(config);
  const keycloak = Keycloak(ctor);

  watch.$once('ready', function (cb) {
    cb && cb();
  });

  keycloak.onReady = function (authenticated) {
    updateWatchVariables(authenticated);
    watch.ready = true;
    typeof options.onReady === 'function' && watch.$emit('ready', options.onReady.bind(this, keycloak));
  };
  keycloak.onAuthSuccess = function () {
    // Check token validity every 10 seconds (10 000 ms) and, if necessary, update the token.
    // Refresh token if it's valid for less then 60 seconds
    const updateTokenInterval = setInterval(() => keycloak.updateToken(60).catch(() => {
      keycloak.clearToken();
    }), 10000);
    watch.logoutFn = () => {
      clearInterval(updateTokenInterval);
      keycloak.logout(options.logout || { 'redirectUri': config['logoutRedirectUri'] });
    };
  };
  keycloak.onAuthRefreshSuccess = function () {
    updateWatchVariables(true);
  };
  keycloak.onAuthLogout = function () {
    updateWatchVariables(false);
  };
  keycloak.init(options.init)
    .catch(err => {
      typeof options.onInitError === 'function' && options.onInitError(err);
    });

  function updateWatchVariables(isAuthenticated = false) {
    watch.authenticated = isAuthenticated;
    watch.loginFn = keycloak.login;
    watch.login = keycloak.login;
    watch.createLoginUrl = keycloak.createLoginUrl;
    watch.createLogoutUrl = keycloak.createLogoutUrl;
    watch.createRegisterUrl = keycloak.createRegisterUrl;
    watch.register = keycloak.register;
    if (isAuthenticated) {
      watch.accountManagement = keycloak.accountManagement;
      watch.createAccountUrl = keycloak.createAccountUrl;
      watch.hasRealmRole = keycloak.hasRealmRole;
      watch.hasResourceRole = keycloak.hasResourceRole;
      watch.loadUserProfile = keycloak.loadUserProfile;
      watch.loadUserInfo = keycloak.loadUserInfo;
      watch.token = keycloak.token;
      watch.subject = keycloak.subject;
      watch.idToken = keycloak.idToken;
      watch.idTokenParsed = keycloak.idTokenParsed;
      watch.realmAccess = keycloak.realmAccess;
      watch.resourceAccess = keycloak.resourceAccess;
      watch.refreshToken = keycloak.refreshToken;
      watch.refreshTokenParsed = keycloak.refreshTokenParsed;
      watch.timeSkew = keycloak.timeSkew;
      watch.responseMode = keycloak.responseMode;
      watch.responseType = keycloak.responseType;
      watch.tokenParsed = keycloak.tokenParsed;
      watch.userName = keycloak.tokenParsed['preferred_username'];
      watch.fullName = keycloak.tokenParsed['name'];
    }
  }
}

function assertOptions(options) {
  const { config, init, onReady, onInitError } = options;
  if (typeof config !== 'string' && !_isObject(config)) {
    return { hasError: true, error: `'config' option must be a string or an object. Found: '${config}'` };
  }
  if (!_isObject(init) || typeof init.onLoad !== 'string') {
    return { hasError: true, error: `'init' option must be an object with an 'onLoad' property. Found: '${init}'` };
  }
  if (onReady && typeof onReady !== 'function') {
    return { hasError: true, error: `'onReady' option must be a function. Found: '${onReady}'` };
  }
  if (onInitError && typeof onInitError !== 'function') {
    return { hasError: true, error: `'onInitError' option must be a function. Found: '${onInitError}'` };
  }
  return {
    hasError: false,
    error: null
  };
}

function _isObject(obj) {
  return obj !== null && typeof obj === 'object' && Object.prototype.toString.call(obj) !== '[object Array]';
}

function getConfig(config) {
  if (_isObject(config)) return Promise.resolve(config);
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', config);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(Error(xhr.statusText));
        }
      }
    };
    xhr.send();
  });
}

function sanitizeConfig(config) {
  const renameProp = (oldProp, newProp, { [oldProp]: old, ...others }) => {
    return {
      [newProp]: old,
      ...others
    };
  };
  return Object.keys(config).reduce(function (previous, key) {
    if (['authRealm', 'authUrl', 'authClientId'].includes(key)) {
      const cleaned = key.replace('auth', '');
      const newKey = cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
      return renameProp(key, newKey, previous);
    }
    return previous;
  }, config);
}