From 69a604044dfb81cabcbeef471abe0da21ddfa93c Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Sun, 6 Oct 2024 14:35:03 +0700 Subject: [PATCH 01/16] Add additional login methods --- default/config.yaml | 13 +++++ server.js | 10 ++-- src/middleware/basicAuth.js | 28 +++++++++-- src/users.js | 98 +++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 8 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index a7e7a747e..69c32af29 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -51,6 +51,19 @@ requestProxy: enableUserAccounts: false # Enable discreet login mode: hides user list on the login screen enableDiscreetLogin: false +# Enable's authlia based auto login. Only enable this if you +# have setup and installed Authelia as a middle-ware on your +# reverse proxy +# https://www.authelia.com/ +# This will use auto login to an account with the same username +# as that used for authlia. (Ensure the username in authlia +# is an exact match with that in sillytavern) +autheliaAuth: false +# If `basicAuthMode` and this are enabled then +# the username and passwords for basic auth are the same as those +# for the individual accounts +perUserBasicAuth: false + # User session timeout *in seconds* (defaults to 24 hours). ## Set to a positive number to expire session after a certain time of inactivity ## Set to 0 to expire session when the browser is closed diff --git a/server.js b/server.js index 677561517..c85f5a541 100644 --- a/server.js +++ b/server.js @@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true; const DEFAULT_ACCOUNTS = false; const DEFAULT_CSRF_DISABLED = false; const DEFAULT_BASIC_AUTH = false; +const DEFAULT_PERUSER_BASIC_AUTH = false; const DEFAULT_ENABLE_IPV6 = false; const DEFAULT_ENABLE_IPV4 = true; @@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode' const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); +const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH); const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); @@ -756,9 +758,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) { } if (basicAuthMode) { - const basicAuthUser = getConfigValue('basicAuthUser', {}); - if (!basicAuthUser?.username || !basicAuthUser?.password) { - console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); + if (!PERUSER_BASIC_AUTH) { + const basicAuthUser = getConfigValue('basicAuthUser', {}); + if (!basicAuthUser?.username || !basicAuthUser?.password) { + console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); + } } } }; diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 190cc39bd..3d7afa23e 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -2,14 +2,18 @@ * When applied, this middleware will ensure the request contains the required header for basic authentication and only * allow access to the endpoint after successful authentication. */ -const { getConfig } = require('../util.js'); +const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js'); +const { getConfig, getConfigValue } = require('../util.js'); +const storage = require('node-persist'); + +const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); const unauthorizedResponse = (res) => { res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); return res.status(401).send('Authentication required'); }; -const basicAuthMiddleware = function (request, response, callback) { +const basicAuthMiddleware = async function (request, response, callback) { const config = getConfig(); const authHeader = request.headers.authorization; @@ -27,11 +31,25 @@ const basicAuthMiddleware = function (request, response, callback) { .toString('utf8') .split(':'); - if (username === config.basicAuthUser.username && password === config.basicAuthUser.password) { + + if (! PERUSER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { return callback(); - } else { - return unauthorizedResponse(response); + } else if (PERUSER_BASIC_AUTH) { + const userHandles = await getAllUserHandles(); + for (const userHandle of userHandles) { + if (username == userHandle) { + const user = await storage.getItem(toKey(userHandle)); + if (user && (user.password && user.password === getPasswordHash(password, user.salt))) { + return callback(); + } + else if (user && !user.password && !password) { + // Login to an account without password + return callback(); + } + } + } } + return unauthorizedResponse(response); }; module.exports = basicAuthMiddleware; diff --git a/src/users.js b/src/users.js index 7ef260ac0..93d29d323 100644 --- a/src/users.js +++ b/src/users.js @@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets'); const KEY_PREFIX = 'user:'; const AVATAR_PREFIX = 'avatar:'; const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); +const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); +const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); /** @@ -575,6 +577,31 @@ async function tryAutoLogin(request) { return false; } + if (await singler_user_login(request)) { + return true; + } + + if (AUTHELIA_AUTH && await authelia_user_login(request)) { + return true; + } + + if (PERUSER_BASIC_AUTH && await basic_user_login(request)) { + return true; + } + + return false; +} + +/** + * Tries auto-login if there is only one user and it's not password protected. + * @param {import('express').Request} request Request object + * @returns {Promise} Whether auto-login was performed + */ +async function singler_user_login(request) { + if (!request.session) { + return false; + } + const userHandles = await getAllUserHandles(); if (userHandles.length === 1) { const user = await storage.getItem(toKey(userHandles[0])); @@ -583,7 +610,78 @@ async function tryAutoLogin(request) { return true; } } + return false; +} +/** + * Tries auto-login with authlia trusted headers. + * https://www.authelia.com/integration/trusted-header-sso/introduction/ + * @param {import('express').Request} request Request object + * @returns {Promise} Whether auto-login was performed + */ +async function authelia_user_login(request) { + if (!request.session) { + return false; + } + + const remote_user = request.get("Remote-User"); + if (!remote_user) { + return false; + } + + const userHandles = await getAllUserHandles(); + for (const userHandle of userHandles) { + if (remote_user == userHandle) { + const user = await storage.getItem(toKey(userHandle)); + if (user) { + request.session.handle = userHandle; + return true; + } + } + } + return false; +} + +/** + * Tries auto-login with basic auth username. + * @param {import('express').Request} request Request object + * @returns {Promise} Whether auto-login was performed + */ +async function basic_user_login(request) { + if (!request.session) { + return false; + } + + const auth_header = request.get("Authorization"); + if (!auth_header) { + return false; + } + + const parts = auth_header.split(' '); + if (!parts || parts.length < 2 || parts[0].toLowerCase() != "basic") { + return false; + } + + const b64auth = parts[1]; + const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':') + + const userHandles = await getAllUserHandles(); + for (const userHandle of userHandles) { + if (login == userHandle) { + const user = await storage.getItem(toKey(userHandle)); + // Verify pass again here just to be sure + if (user && user.password && user.password === getPasswordHash(password, user.salt)) { + request.session.handle = userHandle; + return true; + } + else if (user && !user.password && !password) { + // Login to an account without password + request.session.handle = userHandle; + return true; + } + } + } + return false; } From 71236e5e8c9183e8a3910a190e30ee425bec614d Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Sun, 6 Oct 2024 15:06:05 +0700 Subject: [PATCH 02/16] eslint --- src/middleware/basicAuth.js | 1 - src/users.js | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 3d7afa23e..d6e5ba3f2 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -31,7 +31,6 @@ const basicAuthMiddleware = async function (request, response, callback) { .toString('utf8') .split(':'); - if (! PERUSER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { return callback(); } else if (PERUSER_BASIC_AUTH) { diff --git a/src/users.js b/src/users.js index 93d29d323..d1657a39c 100644 --- a/src/users.js +++ b/src/users.js @@ -624,7 +624,7 @@ async function authelia_user_login(request) { return false; } - const remote_user = request.get("Remote-User"); + const remote_user = request.get('Remote-User'); if (!remote_user) { return false; } @@ -652,18 +652,18 @@ async function basic_user_login(request) { return false; } - const auth_header = request.get("Authorization"); + const auth_header = request.get('Authorization'); if (!auth_header) { return false; } const parts = auth_header.split(' '); - if (!parts || parts.length < 2 || parts[0].toLowerCase() != "basic") { + if (!parts || parts.length < 2 || parts[0].toLowerCase() != 'basic') { return false; } const b64auth = parts[1]; - const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':') + const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':'); const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { @@ -681,7 +681,7 @@ async function basic_user_login(request) { } } } - + return false; } From 329469021e43986004be069c4a7167bcb39eb6ae Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Sun, 6 Oct 2024 18:20:30 +0700 Subject: [PATCH 03/16] Address comments --- src/users.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/users.js b/src/users.js index d1657a39c..a00e66133 100644 --- a/src/users.js +++ b/src/users.js @@ -577,15 +577,15 @@ async function tryAutoLogin(request) { return false; } - if (await singler_user_login(request)) { + if (await singleUserLogin(request)) { return true; } - if (AUTHELIA_AUTH && await authelia_user_login(request)) { + if (AUTHELIA_AUTH && await autheliaUserLogin(request)) { return true; } - if (PERUSER_BASIC_AUTH && await basic_user_login(request)) { + if (PERUSER_BASIC_AUTH && await basicUserLogin(request)) { return true; } @@ -597,7 +597,7 @@ async function tryAutoLogin(request) { * @param {import('express').Request} request Request object * @returns {Promise} Whether auto-login was performed */ -async function singler_user_login(request) { +async function singleUserLogin(request) { if (!request.session) { return false; } @@ -619,7 +619,7 @@ async function singler_user_login(request) { * @param {import('express').Request} request Request object * @returns {Promise} Whether auto-login was performed */ -async function authelia_user_login(request) { +async function autheliaUserLogin(request) { if (!request.session) { return false; } @@ -647,7 +647,7 @@ async function authelia_user_login(request) { * @param {import('express').Request} request Request object * @returns {Promise} Whether auto-login was performed */ -async function basic_user_login(request) { +async function basicUserLogin(request) { if (!request.session) { return false; } From 1cda7003d1441769239ea06042ccf945aec23286 Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Mon, 7 Oct 2024 09:17:43 +0700 Subject: [PATCH 04/16] Add a noauto query param to login --- public/scripts/login.js | 8 +++++++- public/scripts/user.js | 9 ++++++++- src/users.js | 20 ++++++++++++-------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/public/scripts/login.js b/public/scripts/login.js index e02ec1039..dec6a90e9 100644 --- a/public/scripts/login.js +++ b/public/scripts/login.js @@ -180,7 +180,13 @@ function displayError(message) { * Preserves the query string. */ function redirectToHome() { - window.location.href = '/' + window.location.search; + // After a login theres no need to preserve the + // noauto (if present) + const urlParams = new URLSearchParams(window.location.search); + + urlParams.delete('noauto'); + + window.location.href = '/' + urlParams.toString(); } /** diff --git a/public/scripts/user.js b/public/scripts/user.js index 4aab9bb1b..c5d984e1b 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -848,7 +848,14 @@ async function logout() { headers: getRequestHeaders(), }); - window.location.reload(); + // On an explicit logout stop auto login + // to allow user to change username even + // when auto auth (such as authelia or basic) + // would be valid + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('noauto', 'true'); + + window.location.search = urlParams.toString(); } /** diff --git a/src/users.js b/src/users.js index a00e66133..4da3c81ce 100644 --- a/src/users.js +++ b/src/users.js @@ -569,6 +569,7 @@ function shouldRedirectToLogin(request) { /** * Tries auto-login if there is only one user and it's not password protected. + * or another configured method such authlia or basic * @param {import('express').Request} request Request object * @returns {Promise} Whether auto-login was performed */ @@ -577,16 +578,19 @@ async function tryAutoLogin(request) { return false; } - if (await singleUserLogin(request)) { - return true; - } + console.warn(request.session.noauto); + if (!request.query.noauto) { + if (await singleUserLogin(request)) { + return true; + } - if (AUTHELIA_AUTH && await autheliaUserLogin(request)) { - return true; - } + if (AUTHELIA_AUTH && await autheliaUserLogin(request)) { + return true; + } - if (PERUSER_BASIC_AUTH && await basicUserLogin(request)) { - return true; + if (PERUSER_BASIC_AUTH && await basicUserLogin(request)) { + return true; + } } return false; From b9375ed7ea7b429147e17727586028722b26924f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:37:34 +0300 Subject: [PATCH 05/16] Rename PERUSER => PER_USER --- server.js | 6 +++--- src/middleware/basicAuth.js | 6 +++--- src/users.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server.js b/server.js index c85f5a541..2673e7a48 100644 --- a/server.js +++ b/server.js @@ -65,7 +65,7 @@ const DEFAULT_WHITELIST = true; const DEFAULT_ACCOUNTS = false; const DEFAULT_CSRF_DISABLED = false; const DEFAULT_BASIC_AUTH = false; -const DEFAULT_PERUSER_BASIC_AUTH = false; +const DEFAULT_PER_USER_BASIC_AUTH = false; const DEFAULT_ENABLE_IPV6 = false; const DEFAULT_ENABLE_IPV4 = true; @@ -185,7 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode' const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); -const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH); +const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); @@ -758,7 +758,7 @@ const postSetupTasks = async function (v6Failed, v4Failed) { } if (basicAuthMode) { - if (!PERUSER_BASIC_AUTH) { + if (!perUserBasicAuth) { const basicAuthUser = getConfigValue('basicAuthUser', {}); if (!basicAuthUser?.username || !basicAuthUser?.password) { console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index d6e5ba3f2..6ab40af88 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -6,7 +6,7 @@ const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js'); const { getConfig, getConfigValue } = require('../util.js'); const storage = require('node-persist'); -const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); +const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); const unauthorizedResponse = (res) => { res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); @@ -31,9 +31,9 @@ const basicAuthMiddleware = async function (request, response, callback) { .toString('utf8') .split(':'); - if (! PERUSER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { + if (! PER_USER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { return callback(); - } else if (PERUSER_BASIC_AUTH) { + } else if (PER_USER_BASIC_AUTH) { const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { if (username == userHandle) { diff --git a/src/users.js b/src/users.js index 4da3c81ce..9455e9880 100644 --- a/src/users.js +++ b/src/users.js @@ -20,7 +20,7 @@ const KEY_PREFIX = 'user:'; const AVATAR_PREFIX = 'avatar:'; const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); -const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); +const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); /** @@ -588,7 +588,7 @@ async function tryAutoLogin(request) { return true; } - if (PERUSER_BASIC_AUTH && await basicUserLogin(request)) { + if (PER_USER_BASIC_AUTH && await basicUserLogin(request)) { return true; } } From 8a95e0a2909242bfa0a768a7cf43462ba191e662 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:38:32 +0300 Subject: [PATCH 06/16] [chore] Reformat --- src/middleware/basicAuth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 6ab40af88..4022132a0 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -31,7 +31,7 @@ const basicAuthMiddleware = async function (request, response, callback) { .toString('utf8') .split(':'); - if (! PER_USER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { + if (!PER_USER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { return callback(); } else if (PER_USER_BASIC_AUTH) { const userHandles = await getAllUserHandles(); From 3422303882e0e9c9e2867a51ba0854ee1620d451 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:40:49 +0300 Subject: [PATCH 07/16] [chore] Rename local variable --- src/users.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/users.js b/src/users.js index 9455e9880..54715ef19 100644 --- a/src/users.js +++ b/src/users.js @@ -628,14 +628,14 @@ async function autheliaUserLogin(request) { return false; } - const remote_user = request.get('Remote-User'); - if (!remote_user) { + const remoteUser = request.get('Remote-User'); + if (!remoteUser) { return false; } const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { - if (remote_user == userHandle) { + if (remoteUser == userHandle) { const user = await storage.getItem(toKey(userHandle)); if (user) { request.session.handle = userHandle; From 3e9d0cc1ad2fd0e2308715add311b14d43fd3f03 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:42:51 +0300 Subject: [PATCH 08/16] [chore] Remove debug console log --- src/users.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/users.js b/src/users.js index 54715ef19..b1da76901 100644 --- a/src/users.js +++ b/src/users.js @@ -578,7 +578,6 @@ async function tryAutoLogin(request) { return false; } - console.warn(request.session.noauto); if (!request.query.noauto) { if (await singleUserLogin(request)) { return true; From 07d6808e4e82bc94eb99a451df6480419ff7bb44 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:46:07 +0300 Subject: [PATCH 09/16] [chore] Rename local variable, use strict comparison. --- src/users.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/users.js b/src/users.js index b1da76901..c3a98e02e 100644 --- a/src/users.js +++ b/src/users.js @@ -655,13 +655,13 @@ async function basicUserLogin(request) { return false; } - const auth_header = request.get('Authorization'); - if (!auth_header) { + const authHeader = request.get('Authorization'); + if (!authHeader) { return false; } - const parts = auth_header.split(' '); - if (!parts || parts.length < 2 || parts[0].toLowerCase() != 'basic') { + const parts = authHeader.split(' '); + if (!parts || parts.length < 2 || parts[0].toLowerCase() !== 'basic') { return false; } From 0ada5407ee92d15083bcedc0ab494b4495cd4b44 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:54:56 +0300 Subject: [PATCH 10/16] [bug] Fix login attempts to disabled users --- src/middleware/basicAuth.js | 6 +++--- src/users.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 4022132a0..b18db1209 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -36,12 +36,12 @@ const basicAuthMiddleware = async function (request, response, callback) { } else if (PER_USER_BASIC_AUTH) { const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { - if (username == userHandle) { + if (username === userHandle) { const user = await storage.getItem(toKey(userHandle)); - if (user && (user.password && user.password === getPasswordHash(password, user.salt))) { + if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) { return callback(); } - else if (user && !user.password && !password) { + else if (user && user.enabled && !user.password && !password) { // Login to an account without password return callback(); } diff --git a/src/users.js b/src/users.js index c3a98e02e..8c3c19749 100644 --- a/src/users.js +++ b/src/users.js @@ -636,7 +636,7 @@ async function autheliaUserLogin(request) { for (const userHandle of userHandles) { if (remoteUser == userHandle) { const user = await storage.getItem(toKey(userHandle)); - if (user) { + if (user && user.enabled) { request.session.handle = userHandle; return true; } From a1352d817ae3e07598423b82cc63ad136c74c20d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:04:47 +0300 Subject: [PATCH 11/16] [bug] Don't try per user auto-login if basic auth disabled --- server.js | 2 +- src/users.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 2673e7a48..d39092765 100644 --- a/server.js +++ b/server.js @@ -363,7 +363,7 @@ app.get('/login', async (request, response) => { } try { - const autoLogin = await userModule.tryAutoLogin(request); + const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode); if (autoLogin) { return response.redirect('/'); diff --git a/src/users.js b/src/users.js index 8c3c19749..cf271fc70 100644 --- a/src/users.js +++ b/src/users.js @@ -571,9 +571,10 @@ function shouldRedirectToLogin(request) { * Tries auto-login if there is only one user and it's not password protected. * or another configured method such authlia or basic * @param {import('express').Request} request Request object + * @param {boolean} basicAuthMode If Basic auth mode is enabled * @returns {Promise} Whether auto-login was performed */ -async function tryAutoLogin(request) { +async function tryAutoLogin(request, basicAuthMode) { if (!ENABLE_ACCOUNTS || request.user || !request.session) { return false; } @@ -587,7 +588,7 @@ async function tryAutoLogin(request) { return true; } - if (PER_USER_BASIC_AUTH && await basicUserLogin(request)) { + if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) { return true; } } From fe8ffe5be85ac62886e48bb0ec7a9710ea381103 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:12:20 +0300 Subject: [PATCH 12/16] [bug] Fix basic auto-logins to disabled users --- src/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users.js b/src/users.js index cf271fc70..46282043f 100644 --- a/src/users.js +++ b/src/users.js @@ -674,11 +674,11 @@ async function basicUserLogin(request) { if (login == userHandle) { const user = await storage.getItem(toKey(userHandle)); // Verify pass again here just to be sure - if (user && user.password && user.password === getPasswordHash(password, user.salt)) { + if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) { request.session.handle = userHandle; return true; } - else if (user && !user.password && !password) { + else if (user && user.enabled && !user.password && !password) { // Login to an account without password request.session.handle = userHandle; return true; From 15436d0f2a70af78790b4eb8ecad440ef337bb9f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:13:23 +0300 Subject: [PATCH 13/16] [chore] Strict equality check --- src/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users.js b/src/users.js index 46282043f..175db03f3 100644 --- a/src/users.js +++ b/src/users.js @@ -635,7 +635,7 @@ async function autheliaUserLogin(request) { const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { - if (remoteUser == userHandle) { + if (remoteUser === userHandle) { const user = await storage.getItem(toKey(userHandle)); if (user && user.enabled) { request.session.handle = userHandle; @@ -671,7 +671,7 @@ async function basicUserLogin(request) { const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { - if (login == userHandle) { + if (login === userHandle) { const user = await storage.getItem(toKey(userHandle)); // Verify pass again here just to be sure if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) { From fe1f9fafbd3f93726a5bd4b676463dd162411abb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:43:52 +0300 Subject: [PATCH 14/16] [bug] Don't allow per user auth with accounts disabled --- server.js | 4 +++- src/middleware/basicAuth.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index d39092765..99c8c44d3 100644 --- a/server.js +++ b/server.js @@ -758,7 +758,9 @@ const postSetupTasks = async function (v6Failed, v4Failed) { } if (basicAuthMode) { - if (!perUserBasicAuth) { + if (perUserBasicAuth && !enableAccounts) { + console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.')); + } else if (!perUserBasicAuth) { const basicAuthUser = getConfigValue('basicAuthUser', {}); if (!basicAuthUser?.username || !basicAuthUser?.password) { console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index b18db1209..be7a153eb 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -7,6 +7,7 @@ const { getConfig, getConfigValue } = require('../util.js'); const storage = require('node-persist'); const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); +const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); const unauthorizedResponse = (res) => { res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); @@ -27,13 +28,14 @@ const basicAuthMiddleware = async function (request, response, callback) { return unauthorizedResponse(response); } + const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS; const [username, password] = Buffer.from(credentials, 'base64') .toString('utf8') .split(':'); - if (!PER_USER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { + if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { return callback(); - } else if (PER_USER_BASIC_AUTH) { + } else if (usePerUserAuth) { const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { if (username === userHandle) { From 06a7bdd3ce950cdf7152fc12a95b04f7110bb3d9 Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Wed, 9 Oct 2024 15:04:28 +0700 Subject: [PATCH 15/16] Only allow login via basic per-user if user password is set --- src/middleware/basicAuth.js | 4 ---- src/users.js | 5 ----- 2 files changed, 9 deletions(-) diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index be7a153eb..07408e7bd 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -43,10 +43,6 @@ const basicAuthMiddleware = async function (request, response, callback) { if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) { return callback(); } - else if (user && user.enabled && !user.password && !password) { - // Login to an account without password - return callback(); - } } } } diff --git a/src/users.js b/src/users.js index 175db03f3..8d5d70630 100644 --- a/src/users.js +++ b/src/users.js @@ -678,11 +678,6 @@ async function basicUserLogin(request) { request.session.handle = userHandle; return true; } - else if (user && user.enabled && !user.password && !password) { - // Login to an account without password - request.session.handle = userHandle; - return true; - } } } From ad316c6d78679f27f362d8664e68d43dca0de836 Mon Sep 17 00:00:00 2001 From: QuantumEntangledAndy Date: Wed, 9 Oct 2024 15:09:10 +0700 Subject: [PATCH 16/16] [chore] Use same basic code logic in user as in basicAuth --- src/users.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/users.js b/src/users.js index 8d5d70630..4fb85ab0d 100644 --- a/src/users.js +++ b/src/users.js @@ -656,22 +656,25 @@ async function basicUserLogin(request) { return false; } - const authHeader = request.get('Authorization'); + const authHeader = request.headers.authorization; + if (!authHeader) { return false; } - const parts = authHeader.split(' '); - if (!parts || parts.length < 2 || parts[0].toLowerCase() !== 'basic') { + const [scheme, credentials] = authHeader.split(' '); + + if (scheme !== 'Basic' || !credentials) { return false; } - const b64auth = parts[1]; - const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':'); + const [username, password] = Buffer.from(credentials, 'base64') + .toString('utf8') + .split(':'); const userHandles = await getAllUserHandles(); for (const userHandle of userHandles) { - if (login === userHandle) { + if (username === userHandle) { const user = await storage.getItem(toKey(userHandle)); // Verify pass again here just to be sure if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) {