From 3a486ed973f589a1fe608cda67fbd64211188e94 Mon Sep 17 00:00:00 2001 From: Joseph Livecchi Date: Sat, 4 May 2024 13:29:58 -0400 Subject: [PATCH] Added Header Authentication - Server Part (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added loginMethod config option Added a new config option loginMethod that can be password (default) or header for header authentication. This value is returned to the client to be used on the front end --------- Signed-off-by: dependabot[bot] Signed-off-by: Johannes Löthberg Co-authored-by: DJ Mountney Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Johannes Löthberg Co-authored-by: Matt Fiddaman Co-authored-by: DJ Mountney --- src/account-db.js | 16 ++++++++++++++++ src/app-account.js | 32 +++++++++++++++++++++++++++++--- src/config-types.ts | 2 ++ src/load-config.js | 17 +++++++++++++++++ src/util/validate-user.js | 26 ++++++++++++++++++++++++++ upcoming-release-notes/312.md | 6 ++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 upcoming-release-notes/312.md diff --git a/src/account-db.js b/src/account-db.js index 4d5c4892b..cc2fe5674 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -25,6 +25,22 @@ export function needsBootstrap() { return rows.length === 0; } +/* + * Get the Login Method in the following order + * req (the frontend can say which method in the case it wants to resort to forcing password auth) + * config options + * fall back to using password + */ +export function getLoginMethod(req) { + if ( + typeof req !== 'undefined' && + (req.body || { loginMethod: null }).loginMethod + ) { + return req.body.loginMethod; + } + return config.loginMethod || 'password'; +} + export function bootstrap(password) { if (password === undefined || password === '') { return { error: 'invalid-password' }; diff --git a/src/app-account.js b/src/app-account.js index d26babdb0..5969313db 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -1,11 +1,12 @@ import express from 'express'; import errorMiddleware from './util/error-middleware.js'; -import validateUser from './util/validate-user.js'; +import validateUser, { validateAuthHeader } from './util/validate-user.js'; import { bootstrap, login, changePassword, needsBootstrap, + getLoginMethod, } from './account-db.js'; let app = express(); @@ -22,7 +23,7 @@ export { app as handlers }; app.get('/needs-bootstrap', (req, res) => { res.send({ status: 'ok', - data: { bootstrapped: !needsBootstrap() }, + data: { bootstrapped: !needsBootstrap(), loginMethod: getLoginMethod() }, }); }); @@ -38,7 +39,32 @@ app.post('/bootstrap', (req, res) => { }); app.post('/login', (req, res) => { - let { error, token } = login(req.body.password); + let loginMethod = getLoginMethod(req); + console.log('Logging in via ' + loginMethod); + let tokenRes = null; + switch (loginMethod) { + case 'header': { + let headerVal = req.get('x-actual-password') || ''; + console.debug('HEADER VALUE: ' + headerVal); + if (headerVal == '') { + res.send({ status: 'error', reason: 'invalid-header' }); + return; + } else { + if (validateAuthHeader(req)) { + tokenRes = login(headerVal); + } else { + res.send({ status: 'error', reason: 'proxy-not-trusted' }); + return; + } + } + break; + } + case 'password': + default: + tokenRes = login(req.body.password); + break; + } + let { error, token } = tokenRes; if (error) { res.status(400).send({ status: 'error', reason: error }); diff --git a/src/config-types.ts b/src/config-types.ts index df7741b39..8be7ba49d 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -2,6 +2,8 @@ import { ServerOptions } from 'https'; export interface Config { mode: 'test' | 'development'; + loginMethod: 'password' | 'header'; + trustedProxies: string[]; dataDir: string; projectRoot: string; port: number; diff --git a/src/load-config.js b/src/load-config.js index a35edcb4f..d99ce4211 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -48,6 +48,15 @@ if (process.env.ACTUAL_CONFIG_PATH) { /** @type {Omit} */ let defaultConfig = { + loginMethod: 'password', + // assume local networks are trusted for header authentication + trustedProxies: [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + 'fc00::/7', + '::1/128', + ], port: 5006, hostname: '::', webRoot: path.join( @@ -88,6 +97,12 @@ if (process.env.NODE_ENV === 'test') { const finalConfig = { ...config, + loginMethod: process.env.ACTUAL_LOGIN_METHOD + ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() + : config.loginMethod, + trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES + ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) + : config.trustedProxies, port: +process.env.ACTUAL_PORT || +process.env.PORT || config.port, hostname: process.env.ACTUAL_HOSTNAME || config.hostname, serverFiles: process.env.ACTUAL_SERVER_FILES || config.serverFiles, @@ -127,6 +142,8 @@ debug(`using data directory ${finalConfig.dataDir}`); debug(`using server files directory ${finalConfig.serverFiles}`); debug(`using user files directory ${finalConfig.userFiles}`); debug(`using web root directory ${finalConfig.webRoot}`); +debug(`using login method ${finalConfig.loginMethod}`); +debug(`using trusted proxies ${finalConfig.trustedProxies.join(', ')}`); if (finalConfig.https) { debug(`using https key: ${'*'.repeat(finalConfig.https.key.length)}`); diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 9cb319563..117fb779b 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -1,4 +1,7 @@ import { getSession } from '../account-db.js'; +import config from '../load-config.js'; +import proxyaddr from 'proxy-addr'; +import ipaddr from 'ipaddr.js'; /** * @param {import('express').Request} req @@ -25,3 +28,26 @@ export default function validateUser(req, res) { return session; } + +export function validateAuthHeader(req) { + if (config.trustedProxies.length == 0) { + return true; + } + + let sender = proxyaddr(req, 'uniquelocal'); + let sender_ip = ipaddr.process(sender); + const rangeList = { + allowed_ips: config.trustedProxies.map((q) => ipaddr.parseCIDR(q)), + }; + /* eslint-disable @typescript-eslint/ban-ts-comment */ + // @ts-ignore : there is an error in the ts definition for the function, but this is valid + var matched = ipaddr.subnetMatch(sender_ip, rangeList, 'fail'); + /* eslint-enable @typescript-eslint/ban-ts-comment */ + if (matched == 'allowed_ips') { + console.info(`Header Auth Login permitted from ${sender}`); + return true; + } else { + console.warn(`Header Auth Login attempted from ${sender}`); + return false; + } +} diff --git a/upcoming-release-notes/312.md b/upcoming-release-notes/312.md new file mode 100644 index 000000000..1f2abd811 --- /dev/null +++ b/upcoming-release-notes/312.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joewashear007] +--- + +Add option to authenticate with HTTP header from Auth Proxy. \ No newline at end of file