| @@ -51,6 +51,19 @@ requestProxy: | |||
| 51 | enableUserAccounts: false | 51 | enableUserAccounts: false |
| 52 | # Enable discreet login mode: hides user list on the login screen | 52 | # Enable discreet login mode: hides user list on the login screen |
| 53 | enableDiscreetLogin: false | 53 | enableDiscreetLogin: 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 | |||
| 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 inactivity | 68 | ## 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 closed | 69 | ## Set to 0 to expire session when the browser is closed |
| @@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true; | |||
| 65 | const DEFAULT_ACCOUNTS = false; | 65 | const DEFAULT_ACCOUNTS = false; |
| 66 | const DEFAULT_CSRF_DISABLED = false; | 66 | const DEFAULT_CSRF_DISABLED = false; |
| 67 | const DEFAULT_BASIC_AUTH = false; | 67 | const DEFAULT_BASIC_AUTH = false; |
| 68 | const DEFAULT_PERUSER_BASIC_AUTH = false; | ||
| 68 | 69 | ||
| 69 | const DEFAULT_ENABLE_IPV6 = false; | 70 | const DEFAULT_ENABLE_IPV6 = false; |
| 70 | const DEFAULT_ENABLE_IPV4 = true; | 71 | const DEFAULT_ENABLE_IPV4 = true; |
| @@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode' | |||
| 184 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); | 185 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 185 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); | 186 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 186 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); | 187 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| 188 | const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH); | ||
| 187 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); | 189 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 188 | 190 | ||
| 189 | const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); | 191 | const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); |
| @@ -756,11 +758,13 @@ const postSetupTasks = async function (v6Failed, v4Failed) { | |||
| 756 | } | 758 | } |
| 757 | 759 | ||
| 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 | }; |
| 765 | 769 | ||
| 766 | /** | 770 | /** |
| @@ -2,14 +2,18 @@ | |||
| 2 | * When applied, this middleware will ensure the request contains the required header for basic authentication and only | 2 | * 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 | */ |
| 5 | const { getConfig } = require('../util.js'); | 5 | const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js'); |
| 6 | const { getConfig, getConfigValue } = require('../util.js'); | ||
| 7 | const storage = require('node-persist'); | ||
| 8 | |||
| 9 | const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | ||
| 6 | 10 | ||
| 7 | const unauthorizedResponse = (res) => { | 11 | const 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 | }; |
| 11 | 15 | ||
| 12 | const basicAuthMiddleware = function (request, response, callback) { | 16 | const 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; |
| 15 | 19 | ||
| @@ -27,11 +31,25 @@ const basicAuthMiddleware = function (request, response, callback) { | |||
| 27 | .toString('utf8') | 31 | .toString('utf8') |
| 28 | .split(':'); | 32 | .split(':'); |
| 29 | 33 | ||
| 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 | }; |
| 36 | 54 | ||
| 37 | module.exports = basicAuthMiddleware; | 55 | module.exports = basicAuthMiddleware; |
| @@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets'); | |||
| 19 | const KEY_PREFIX = 'user:'; | 19 | const KEY_PREFIX = 'user:'; |
| 20 | const AVATAR_PREFIX = 'avatar:'; | 20 | const AVATAR_PREFIX = 'avatar:'; |
| 21 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); | 21 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); |
| 22 | const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); | ||
| 23 | const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | ||
| 22 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | 24 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
| 23 | 25 | ||
| 24 | /** | 26 | /** |
| @@ -575,6 +577,31 @@ async function tryAutoLogin(request) { | |||
| 575 | return false; | 577 | return false; |
| 576 | } | 578 | } |
| 577 | 579 | ||
| 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 | |||
| 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 | */ | ||
| 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 | } | ||
| 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 | */ | ||
| 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 | } | ||
| 586 | 684 | ||
| 587 | return false; | 685 | return false; |
| 588 | } | 686 | } |