From 4aa34f66dc00555407a140497ff1550634bc3f7d Mon Sep 17 00:00:00 2001 From: Todd E Johnson Date: Fri, 17 Nov 2023 00:00:48 -0600 Subject: [PATCH] WIP: Attempt at adding header auth. Ignore Settings #981 --- server.js | 10 +++++ services/get-user.js | 5 +++ src/main.js | 27 ++++++++----- src/utils/ConfigSchema.json | 31 +++++++++++++++ src/utils/HeaderAuth.js | 77 +++++++++++++++++++++++++++++++++++++ src/utils/defaults.js | 1 + 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 services/get-user.js create mode 100644 src/utils/HeaderAuth.js diff --git a/server.js b/server.js index a5401700..37e55b0d 100644 --- a/server.js +++ b/server.js @@ -27,6 +27,7 @@ const rebuild = require('./services/rebuild-app'); // A script to programmatical const systemInfo = require('./services/system-info'); // Basic system info, for resource widget const sslServer = require('./services/ssl-server'); // TLS-enabled web server const corsProxy = require('./services/cors-proxy'); // Enables API requests to CORS-blocked services +const getUser = require('./services/get-user'); // Enables server side user lookup /* Helper functions, and default config */ const printMessage = require('./services/print-message'); // Function to print welcome msg on start @@ -124,6 +125,15 @@ const app = express() res.end(JSON.stringify({ success: false, message: e })); } }) + // GET endpoint to return user info + .use(ENDPOINTS.getUser, (req, res) => { + try { + const user = getUser(req); + res.end(JSON.stringify(user)); + } catch (e) { + res.end(JSON.stringify({ success: false, message: e })); + } + }) // GET fallback endpoint .get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist', 'index.html'))); diff --git a/services/get-user.js b/services/get-user.js new file mode 100644 index 00000000..bef1e5f8 --- /dev/null +++ b/services/get-user.js @@ -0,0 +1,5 @@ +module.exports = (req) => { + const userHeader = "Remote-User"; + console.log("Running Server Side", req.headers[userHeader.toLowerCase()]); // eslint-disable-line no-console + return { "success": true, "user": req.headers[userHeader.toLowerCase()] }; +}; \ No newline at end of file diff --git a/src/main.js b/src/main.js index c3f626d8..cd7a0df3 100644 --- a/src/main.js +++ b/src/main.js @@ -21,6 +21,7 @@ import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initial import clickOutside from '@/directives/ClickOutside'; // Directive for closing popups, modals, etc import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults'; import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth'; +import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth'; import Keys from '@/utils/StoreMutations'; // Initialize global Vue components @@ -54,18 +55,24 @@ ErrorReporting(Vue, router); // Render function const render = (awesome) => awesome(Dashy); -store.dispatch(Keys.INITIALIZE_CONFIG).then((thing) => console.log('main', thing)); - // Mount the app, with router, store i18n and render func const mount = () => new Vue({ store, router, render, i18n, }).$mount('#app'); -// If Keycloak not enabled, then proceed straight to the app -if (!isKeycloakEnabled()) { - mount(); -} else { // Keycloak is enabled, redirect to KC login page - initKeycloakAuth() - .then(() => mount()) - .catch(() => window.location.reload()); -} +store.dispatch(Keys.INITIALIZE_CONFIG).then((thing) => { + console.log('main', thing); + + // Keycloak is enabled, redirect to KC login page + if (isKeycloakEnabled()) { + initKeycloakAuth() + .then(() => mount()) + .catch(() => window.location.reload()); + } else if (isHeaderAuthEnabled()) { + initHeaderAuth() + .then(() => mount()) + .catch(() => window.location.reload()); + } else { // If Keycloak not enabled, then proceed straight to the app + mount(); + } +}); diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index a0994ae2..c4771454 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -450,6 +450,37 @@ } } }, + "enableHeaderAuth": { + "title": "Enable HeaderAuth?", + "type": "boolean", + "default": false, + "description": "If set to true, enable Header Authentication. See appConfig.auth.headerAuth" + }, + "headerAuth": { + "type": "object", + "description": "Configuration for headerAuth", + "additionalProperties": false, + "required": [ + "proxyWhitelist" + ], + "properties": { + "userHeader": { + "title": "User Header", + "type": "string", + "description": "Header name which contains username", + "default": "REMOTE_USER" + }, + "proxyWhitelist": { + "title": "Upstream Proxy Auth Trust", + "type": "array", + "description": "Upstream proxy servers to expect authenticated requests from", + "items": { + "type": "string", + "description": "IPs of upstream proxies that will be trusted" + } + } + } + }, "enableKeycloak": { "title": "Enable Keycloak?", "type": "boolean", diff --git a/src/utils/HeaderAuth.js b/src/utils/HeaderAuth.js new file mode 100644 index 00000000..5dc52367 --- /dev/null +++ b/src/utils/HeaderAuth.js @@ -0,0 +1,77 @@ +import axios from 'axios'; +import sha256 from 'crypto-js/sha256'; +import ConfigAccumulator from '@/utils/ConfigAccumalator'; +import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults'; +import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler'; +import { logout, getUserState } from '@/utils/Auth'; + +const getAppConfig = () => { + const Accumulator = new ConfigAccumulator(); + const config = Accumulator.config(); + return config.appConfig || {}; +}; + +class HeaderAuth { + constructor() { + const { auth } = getAppConfig(); + const { + userHeader, proxyWhitelist, + } = auth.headerAuth; + this.userHeader = userHeader; + this.proxyWhitelist = proxyWhitelist; + this.users = auth.users; + } + + /* eslint-disable class-methods-use-this */ + login() { + return new Promise((resolve, reject) => { + const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin; + axios.get(`${baseUrl}${serviceEndpoints.getUser}`).then((response) => { + if (!response.data || response.data.errorMsg) { + reject(response.data.errorMsg || 'Error'); + } else { + try { + this.users.forEach((user) => { + if (user.user.toLowerCase() === response.data.user.toLowerCase()) { // User found + const strAndUpper = (input) => input.toString().toUpperCase(); + const sha = strAndUpper(sha256(strAndUpper(user.user) + strAndUpper(user.hash))); + document.cookie = `${cookieKeys.AUTH_TOKEN}=${sha};`; + localStorage.setItem(localStorageKeys.USERNAME, user.user); + InfoHandler(`Succesfully signed in as ${response.data.user}`, InfoKeys.AUTH); + console.log('I think we\'re good', getUserState()); + resolve(response.data.user); + } + }); + } catch (e) { + reject(e); + } + } + }); + }); + } + + logout() { + logout(); + } +} + +export const isHeaderAuthEnabled = () => { + const { auth } = getAppConfig(); + if (!auth) return false; + return auth.enableHeaderAuth || false; +}; + +let headerAuth; + +export const initHeaderAuth = () => { + headerAuth = new HeaderAuth(); + return headerAuth.login(); +}; + +// TODO: Find where this is implemented +export const getHeaderAuth = () => { + if (!headerAuth) { + ErrorHandler("HeaderAuth not initialized, can't get instance of class"); + } + return headerAuth; +}; diff --git a/src/utils/defaults.js b/src/utils/defaults.js index 3363aca1..508cf011 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -44,6 +44,7 @@ module.exports = { rebuild: '/config-manager/rebuild', systemInfo: '/system-info', corsProxy: '/cors-proxy', + getUser: '/get-user', }, /* List of built-in themes, to be displayed within the theme-switcher dropdown */ builtInThemes: [