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:
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
public/scripts/login.js+7 -1
@@ -180,7 +180,13 @@ function displayError(message) {
180 * Preserves the query string.180 * Preserves the query string.
181 */181 */
182function redirectToHome() {182function 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();
184}190}
185191
186/**192/**
public/scripts/user.js+8 -1
@@ -848,7 +848,14 @@ async function logout() {
848 headers: getRequestHeaders(),848 headers: getRequestHeaders(),
849 });849 });
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();
852}859}
853860
854/**861/**
server.js+7 -1
@@ -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_PER_USER_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 perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_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);
@@ -361,7 +363,7 @@ app.get('/login', async (request, response) => {
361 }363 }
362364
363 try {365 try {
364 const autoLogin = await userModule.tryAutoLogin(request);366 const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode);
365367
366 if (autoLogin) {368 if (autoLogin) {
367 return response.redirect('/');369 return response.redirect('/');
@@ -756,11 +758,15 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
756 }758 }
757759
758 if (basicAuthMode) {760 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) {
759 const basicAuthUser = getConfigValue('basicAuthUser', {});764 const basicAuthUser = getConfigValue('basicAuthUser', {});
760 if (!basicAuthUser?.username || !basicAuthUser?.password) {765 if (!basicAuthUser?.username || !basicAuthUser?.password) {
761 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));766 console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
762 }767 }
763 }768 }
769 }
764};770};
765771
766/**772/**
src/middleware/basicAuth.js+20 -5
@@ -2,14 +2,19 @@
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 PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
10const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
611
7const unauthorizedResponse = (res) => {12const unauthorizedResponse = (res) => {
8 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');13 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
9 return res.status(401).send('Authentication required');14 return res.status(401).send('Authentication required');
10};15};
1116
12const basicAuthMiddleware = function (request, response, callback) {17const basicAuthMiddleware = async function (request, response, callback) {
13 const config = getConfig();18 const config = getConfig();
14 const authHeader = request.headers.authorization;19 const authHeader = request.headers.authorization;
1520
@@ -23,15 +28,25 @@ const basicAuthMiddleware = function (request, response, callback) {
23 return unauthorizedResponse(response);28 return unauthorizedResponse(response);
24 }29 }
2530
31 const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS;
26 const [username, password] = Buffer.from(credentials, 'base64')32 const [username, password] = Buffer.from(credentials, 'base64')
27 .toString('utf8')33 .toString('utf8')
28 .split(':');34 .split(':');
2935
30 if (username === config.basicAuthUser.username && password === config.basicAuthUser.password) {36 if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
31 return callback();37 return callback();
32 } else {38 } 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 }
34 }46 }
47 }
48 }
49 return unauthorizedResponse(response);
35};50};
3651
37module.exports = basicAuthMiddleware;52module.exports = basicAuthMiddleware;
src/users.js+101 -1
@@ -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 PER_USER_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/**
@@ -567,14 +569,43 @@ function shouldRedirectToLogin(request) {
567569
568/**570/**
569 * Tries auto-login if there is only one user and it's not password protected.571 * Tries auto-login if there is only one user and it's not password protected.
572 * or another configured method such authlia or basic
570 * @param {import('express').Request} request Request object573 * @param {import('express').Request} request Request object
574 * @param {boolean} basicAuthMode If Basic auth mode is enabled
571 * @returns {Promise<boolean>} Whether auto-login was performed575 * @returns {Promise<boolean>} Whether auto-login was performed
572 */576 */
573async function tryAutoLogin(request) {577async function tryAutoLogin(request, basicAuthMode) {
574 if (!ENABLE_ACCOUNTS || request.user || !request.session) {578 if (!ENABLE_ACCOUNTS || request.user || !request.session) {
575 return false;579 return false;
576 }580 }
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 */
604async function singleUserLogin(request) {
605 if (!request.session) {
606 return false;
607 }
608
578 const userHandles = await getAllUserHandles();609 const userHandles = await getAllUserHandles();
579 if (userHandles.length === 1) {610 if (userHandles.length === 1) {
580 const user = await storage.getItem(toKey(userHandles[0]));611 const user = await storage.getItem(toKey(userHandles[0]));
@@ -583,6 +614,75 @@ async function tryAutoLogin(request) {
583 return true;614 return true;
584 }615 }
585 }616 }
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 */
626async 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 */
654async 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
587 return false;687 return false;
588}688}