Merge pull request #2953 from QuantumEntangledAndy/feat/AdditionalLogins Add additional login methods

e7fe2188104c1dc1c57be4fe0f645f76efd69eec

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +156 -9Showing whitespace changes
default/config.yaml+13 -0
@@ -51,6 +51,19 @@ requestProxy:
5151enableUserAccounts: false
5252# Enable discreet login mode: hides user list on the login screen
5353enableDiscreetLogin: false
54+# Enable's authlia based auto login. Only enable this if you
55+# have setup and installed Authelia as a middle-ware on your
56+# reverse proxy
57+# https://www.authelia.com/
58+# This will use auto login to an account with the same username
59+# as that used for authlia. (Ensure the username in authlia
60+# is an exact match with that in sillytavern)
61+autheliaAuth: false
62+# If `basicAuthMode` and this are enabled then
63+# the username and passwords for basic auth are the same as those
64+# for the individual accounts
65+perUserBasicAuth: false
66+
5467# User session timeout *in seconds* (defaults to 24 hours).
5568## Set to a positive number to expire session after a certain time of inactivity
5669## Set to 0 to expire session when the browser is closed
public/scripts/login.js+7 -1
@@ -180,7 +180,13 @@ function displayError(message) {
180180 * Preserves the query string.
181181 */
182182function redirectToHome() {
183- window.location.href = '/' + window.location.search;
183+ // After a login theres no need to preserve the
184+ // noauto (if present)
185+ const urlParams = new URLSearchParams(window.location.search);
186+
187+ urlParams.delete('noauto');
188+
189+ window.location.href = '/' + urlParams.toString();
184190}
185191
186192/**
public/scripts/user.js+8 -1
@@ -848,7 +848,14 @@ async function logout() {
848848 headers: getRequestHeaders(),
849849 });
850850
851- window.location.reload();
851+ // On an explicit logout stop auto login
852+ // to allow user to change username even
853+ // when auto auth (such as authelia or basic)
854+ // would be valid
855+ const urlParams = new URLSearchParams(window.location.search);
856+ urlParams.set('noauto', 'true');
857+
858+ window.location.search = urlParams.toString();
852859}
853860
854861/**
server.js+7 -1
@@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true;
6565const DEFAULT_ACCOUNTS = false;
6666const DEFAULT_CSRF_DISABLED = false;
6767const DEFAULT_BASIC_AUTH = false;
68+const DEFAULT_PER_USER_BASIC_AUTH = false;
6869
6970const DEFAULT_ENABLE_IPV6 = false;
7071const DEFAULT_ENABLE_IPV4 = true;
@@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode'
184185const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
185186const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
186187const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
188+const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
187189const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
188190
189191const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
@@ -361,7 +363,7 @@ app.get('/login', async (request, response) => {
361363 }
362364
363365 try {
364366 const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode);
365367
366368 if (autoLogin) {
367369 return response.redirect('/');
@@ -756,11 +758,15 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
756758 }
757759
758760 if (basicAuthMode) {
761+ if (perUserBasicAuth && !enableAccounts) {
762+ console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));
763+ } else if (!perUserBasicAuth) {
759764 const basicAuthUser = getConfigValue('basicAuthUser', {});
760765 if (!basicAuthUser?.username || !basicAuthUser?.password) {
761766 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
762767 }
763768 }
769+ }
764770};
765771
766772/**
src/middleware/basicAuth.js+20 -5
@@ -2,14 +2,19 @@
22 * When applied, this middleware will ensure the request contains the required header for basic authentication and only
33 * allow access to the endpoint after successful authentication.
44 */
55const { getConfiggetAllUserHandles, toKey, getPasswordHash } = require('../utilusers.js');
6+const { getConfig, getConfigValue } = require('../util.js');
7+const storage = require('node-persist');
8+
9+const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
10+const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
611
712const unauthorizedResponse = (res) => {
813 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
914 return res.status(401).send('Authentication required');
1015};
1116
1217const basicAuthMiddleware = async function (request, response, callback) {
1318 const config = getConfig();
1419 const authHeader = request.headers.authorization;
1520
@@ -23,15 +28,25 @@ const basicAuthMiddleware = function (request, response, callback) {
2328 return unauthorizedResponse(response);
2429 }
2530
31+ const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS;
2632 const [username, password] = Buffer.from(credentials, 'base64')
2733 .toString('utf8')
2834 .split(':');
2935
3036 if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
3137 return callback();
3238 } else if (usePerUserAuth) {
33- return unauthorizedResponse(response);
39+ const userHandles = await getAllUserHandles();
40+ for (const userHandle of userHandles) {
41+ if (username === userHandle) {
42+ const user = await storage.getItem(toKey(userHandle));
43+ if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) {
44+ return callback();
45+ }
3446 }
47+ }
48+ }
49+ return unauthorizedResponse(response);
3550};
3651
3752module.exports = basicAuthMiddleware;
src/users.js+101 -1
@@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets');
1919const KEY_PREFIX = 'user:';
2020const AVATAR_PREFIX = 'avatar:';
2121const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
22+const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);
23+const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
2224const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2325
2426/**
@@ -567,14 +569,43 @@ function shouldRedirectToLogin(request) {
567569
568570/**
569571 * Tries auto-login if there is only one user and it's not password protected.
572+ * or another configured method such authlia or basic
570573 * @param {import('express').Request} request Request object
574+ * @param {boolean} basicAuthMode If Basic auth mode is enabled
571575 * @returns {Promise<boolean>} Whether auto-login was performed
572576 */
573577async function tryAutoLogin(request, basicAuthMode) {
574578 if (!ENABLE_ACCOUNTS || request.user || !request.session) {
575579 return false;
576580 }
577581
582+ if (!request.query.noauto) {
583+ if (await singleUserLogin(request)) {
584+ return true;
585+ }
586+
587+ if (AUTHELIA_AUTH && await autheliaUserLogin(request)) {
588+ return true;
589+ }
590+
591+ if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) {
592+ return true;
593+ }
594+ }
595+
596+ return false;
597+}
598+
599+/**
600+ * Tries auto-login if there is only one user and it's not password protected.
601+ * @param {import('express').Request} request Request object
602+ * @returns {Promise<boolean>} Whether auto-login was performed
603+ */
604+async function singleUserLogin(request) {
605+ if (!request.session) {
606+ return false;
607+ }
608+
578609 const userHandles = await getAllUserHandles();
579610 if (userHandles.length === 1) {
580611 const user = await storage.getItem(toKey(userHandles[0]));
@@ -583,6 +614,75 @@ async function tryAutoLogin(request) {
583614 return true;
584615 }
585616 }
617+ return false;
618+}
619+
620+/**
621+ * Tries auto-login with authlia trusted headers.
622+ * https://www.authelia.com/integration/trusted-header-sso/introduction/
623+ * @param {import('express').Request} request Request object
624+ * @returns {Promise<boolean>} Whether auto-login was performed
625+ */
626+async function autheliaUserLogin(request) {
627+ if (!request.session) {
628+ return false;
629+ }
630+
631+ const remoteUser = request.get('Remote-User');
632+ if (!remoteUser) {
633+ return false;
634+ }
635+
636+ const userHandles = await getAllUserHandles();
637+ for (const userHandle of userHandles) {
638+ if (remoteUser === userHandle) {
639+ const user = await storage.getItem(toKey(userHandle));
640+ if (user && user.enabled) {
641+ request.session.handle = userHandle;
642+ return true;
643+ }
644+ }
645+ }
646+ return false;
647+}
648+
649+/**
650+ * Tries auto-login with basic auth username.
651+ * @param {import('express').Request} request Request object
652+ * @returns {Promise<boolean>} Whether auto-login was performed
653+ */
654+async function basicUserLogin(request) {
655+ if (!request.session) {
656+ return false;
657+ }
658+
659+ const authHeader = request.headers.authorization;
660+
661+ if (!authHeader) {
662+ return false;
663+ }
664+
665+ const [scheme, credentials] = authHeader.split(' ');
666+
667+ if (scheme !== 'Basic' || !credentials) {
668+ return false;
669+ }
670+
671+ const [username, password] = Buffer.from(credentials, 'base64')
672+ .toString('utf8')
673+ .split(':');
674+
675+ const userHandles = await getAllUserHandles();
676+ for (const userHandle of userHandles) {
677+ if (username === userHandle) {
678+ const user = await storage.getItem(toKey(userHandle));
679+ // Verify pass again here just to be sure
680+ if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) {
681+ request.session.handle = userHandle;
682+ return true;
683+ }
684+ }
685+ }
586686
587687 return false;
588688}