Add additional login methods

69a604044dfb81cabcbeef471abe0da21ddfa93c

QuantumEntangledAndy <sheepchaan@gmail.com>

Signed
4 files changed, +138 -5Showing whitespace changes
default/config.yaml+13 -0
@@ -51,6 +51,19 @@ requestProxy:
51enableUserAccounts: false51enableUserAccounts: false
52# Enable discreet login mode: hides user list on the login screen52# Enable discreet login mode: hides user list on the login screen
53enableDiscreetLogin: false53enableDiscreetLogin: 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)
61autheliaAuth: 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
65perUserBasicAuth: false
66
54# User session timeout *in seconds* (defaults to 24 hours).67# User session timeout *in seconds* (defaults to 24 hours).
55## Set to a positive number to expire session after a certain time of inactivity68## Set to a positive number to expire session after a certain time of inactivity
56## Set to 0 to expire session when the browser is closed69## Set to 0 to expire session when the browser is closed
server.js+4 -0
@@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true;
65const DEFAULT_ACCOUNTS = false;65const DEFAULT_ACCOUNTS = false;
66const DEFAULT_CSRF_DISABLED = false;66const DEFAULT_CSRF_DISABLED = false;
67const DEFAULT_BASIC_AUTH = false;67const DEFAULT_BASIC_AUTH = false;
68const DEFAULT_PERUSER_BASIC_AUTH = false;
6869
69const DEFAULT_ENABLE_IPV6 = false;70const DEFAULT_ENABLE_IPV6 = false;
70const DEFAULT_ENABLE_IPV4 = true;71const DEFAULT_ENABLE_IPV4 = true;
@@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode'
184const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');185const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
185const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);186const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
186const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);187const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
188const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH);
187const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);189const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
188190
189const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);191const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
@@ -756,11 +758,13 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
756 }758 }
757759
758 if (basicAuthMode) {760 if (basicAuthMode) {
761 if (!PERUSER_BASIC_AUTH) {
759 const basicAuthUser = getConfigValue('basicAuthUser', {});762 const basicAuthUser = getConfigValue('basicAuthUser', {});
760 if (!basicAuthUser?.username || !basicAuthUser?.password) {763 if (!basicAuthUser?.username || !basicAuthUser?.password) {
761 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));764 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
762 }765 }
763 }766 }
767 }
764};768};
765769
766/**770/**
src/middleware/basicAuth.js+23 -5
@@ -2,14 +2,18 @@
2 * When applied, this middleware will ensure the request contains the required header for basic authentication and only2 * When applied, this middleware will ensure the request contains the required header for basic authentication and only
3 * allow access to the endpoint after successful authentication.3 * allow access to the endpoint after successful authentication.
4 */4 */
5const { getConfig } = require('../util.js');5const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js');
6const { getConfig, getConfigValue } = require('../util.js');
7const storage = require('node-persist');
8
9const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
610
7const unauthorizedResponse = (res) => {11const unauthorizedResponse = (res) => {
8 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');12 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
9 return res.status(401).send('Authentication required');13 return res.status(401).send('Authentication required');
10};14};
1115
12const basicAuthMiddleware = function (request, response, callback) {16const basicAuthMiddleware = async function (request, response, callback) {
13 const config = getConfig();17 const config = getConfig();
14 const authHeader = request.headers.authorization;18 const authHeader = request.headers.authorization;
1519
@@ -27,11 +31,25 @@ const basicAuthMiddleware = function (request, response, callback) {
27 .toString('utf8')31 .toString('utf8')
28 .split(':');32 .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) {
31 return callback();36 return callback();
32 } else {37 } 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 }
34 }50 }
51 }
52 return unauthorizedResponse(response);
35};53};
3654
37module.exports = basicAuthMiddleware;55module.exports = basicAuthMiddleware;
src/users.js+98 -0
@@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets');
19const KEY_PREFIX = 'user:';19const KEY_PREFIX = 'user:';
20const AVATAR_PREFIX = 'avatar:';20const AVATAR_PREFIX = 'avatar:';
21const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);21const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
22const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);
23const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
22const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');24const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2325
24/**26/**
@@ -575,6 +577,31 @@ async function tryAutoLogin(request) {
575 return false;577 return false;
576 }578 }
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 */
600async function singler_user_login(request) {
601 if (!request.session) {
602 return false;
603 }
604
578 const userHandles = await getAllUserHandles();605 const userHandles = await getAllUserHandles();
579 if (userHandles.length === 1) {606 if (userHandles.length === 1) {
580 const user = await storage.getItem(toKey(userHandles[0]));607 const user = await storage.getItem(toKey(userHandles[0]));
@@ -583,6 +610,77 @@ async function tryAutoLogin(request) {
583 return true;610 return true;
584 }611 }
585 }612 }
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 */
622async 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}
644
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 */
650async 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 }
586 684
587 return false;685 return false;
588}686}