Add additional login methods

69a604044dfb81cabcbeef471abe0da21ddfa93c

QuantumEntangledAndy <sheepchaan@gmail.com>

Signed
4 files changed, +141 -8Ignore whitespace
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
server.js+7 -3
@@ -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_PERUSER_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 PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH);
187189const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
188190
189191const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
@@ -756,9 +758,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
756758 }
757759
758760 if (basicAuthMode) {
759- const basicAuthUser = getConfigValue('basicAuthUser', {});
761+ if (!PERUSER_BASIC_AUTH) {
760- if (!basicAuthUser?.username || !basicAuthUser?.password) {
762+ const basicAuthUser = getConfigValue('basicAuthUser', {});
761- console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
763+ if (!basicAuthUser?.username || !basicAuthUser?.password) {
764+ console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
765+ }
762766 }
763767 }
764768};
src/middleware/basicAuth.js+23 -5
@@ -2,14 +2,18 @@
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 PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
610
711const unauthorizedResponse = (res) => {
812 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
913 return res.status(401).send('Authentication required');
1014};
1115
1216const basicAuthMiddleware = async function (request, response, callback) {
1317 const config = getConfig();
1418 const authHeader = request.headers.authorization;
1519
@@ -27,11 +31,25 @@ const basicAuthMiddleware = function (request, response, callback) {
2731 .toString('utf8')
2832 .split(':');
2933
30- if (username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
34+
35+ if (! PERUSER_BASIC_AUTH && username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
3136 return callback();
3237 } else if (PERUSER_BASIC_AUTH) {
33- return unauthorizedResponse(response);
38+ const userHandles = await getAllUserHandles();
39+ for (const userHandle of userHandles) {
40+ if (username == userHandle) {
41+ const user = await storage.getItem(toKey(userHandle));
42+ if (user && (user.password && user.password === getPasswordHash(password, user.salt))) {
43+ return callback();
44+ }
45+ else if (user && !user.password && !password) {
46+ // Login to an account without password
47+ return callback();
48+ }
49+ }
50+ }
3451 }
52+ return unauthorizedResponse(response);
3553};
3654
3755module.exports = basicAuthMiddleware;
src/users.js+98 -0
@@ -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 PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
2224const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2325
2426/**
@@ -575,6 +577,31 @@ async function tryAutoLogin(request) {
575577 return false;
576578 }
577579
580+ if (await singler_user_login(request)) {
581+ return true;
582+ }
583+
584+ if (AUTHELIA_AUTH && await authelia_user_login(request)) {
585+ return true;
586+ }
587+
588+ if (PERUSER_BASIC_AUTH && await basic_user_login(request)) {
589+ return true;
590+ }
591+
592+ return false;
593+}
594+
595+/**
596+ * Tries auto-login if there is only one user and it's not password protected.
597+ * @param {import('express').Request} request Request object
598+ * @returns {Promise<boolean>} Whether auto-login was performed
599+ */
600+async function singler_user_login(request) {
601+ if (!request.session) {
602+ return false;
603+ }
604+
578605 const userHandles = await getAllUserHandles();
579606 if (userHandles.length === 1) {
580607 const user = await storage.getItem(toKey(userHandles[0]));
@@ -583,7 +610,78 @@ async function tryAutoLogin(request) {
583610 return true;
584611 }
585612 }
613+ return false;
614+}
615+
616+/**
617+ * Tries auto-login with authlia trusted headers.
618+ * https://www.authelia.com/integration/trusted-header-sso/introduction/
619+ * @param {import('express').Request} request Request object
620+ * @returns {Promise<boolean>} Whether auto-login was performed
621+ */
622+async function authelia_user_login(request) {
623+ if (!request.session) {
624+ return false;
625+ }
626+
627+ const remote_user = request.get("Remote-User");
628+ if (!remote_user) {
629+ return false;
630+ }
631+
632+ const userHandles = await getAllUserHandles();
633+ for (const userHandle of userHandles) {
634+ if (remote_user == userHandle) {
635+ const user = await storage.getItem(toKey(userHandle));
636+ if (user) {
637+ request.session.handle = userHandle;
638+ return true;
639+ }
640+ }
641+ }
642+ return false;
643+}
586644
645+/**
646+ * Tries auto-login with basic auth username.
647+ * @param {import('express').Request} request Request object
648+ * @returns {Promise<boolean>} Whether auto-login was performed
649+ */
650+async function basic_user_login(request) {
651+ if (!request.session) {
652+ return false;
653+ }
654+
655+ const auth_header = request.get("Authorization");
656+ if (!auth_header) {
657+ return false;
658+ }
659+
660+ const parts = auth_header.split(' ');
661+ if (!parts || parts.length < 2 || parts[0].toLowerCase() != "basic") {
662+ return false;
663+ }
664+
665+ const b64auth = parts[1];
666+ const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':')
667+
668+ const userHandles = await getAllUserHandles();
669+ for (const userHandle of userHandles) {
670+ if (login == userHandle) {
671+ const user = await storage.getItem(toKey(userHandle));
672+ // Verify pass again here just to be sure
673+ if (user && user.password && user.password === getPasswordHash(password, user.salt)) {
674+ request.session.handle = userHandle;
675+ return true;
676+ }
677+ else if (user && !user.password && !password) {
678+ // Login to an account without password
679+ request.session.handle = userHandle;
680+ return true;
681+ }
682+ }
683+ }
684+
587685 return false;
588686}
589687