| @@ -51,6 +51,19 @@ requestProxy: | ||
| 51 | 51 | enableUserAccounts: false |
| 52 | 52 | # Enable discreet login mode: hides user list on the login screen |
| 53 | 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 | 67 | # User session timeout *in seconds* (defaults to 24 hours). |
| 55 | 68 | ## Set to a positive number to expire session after a certain time of inactivity |
| 56 | 69 | ## Set to 0 to expire session when the browser is closed |
| @@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true; | ||
| 65 | 65 | const DEFAULT_ACCOUNTS = false; |
| 66 | 66 | const DEFAULT_CSRF_DISABLED = false; |
| 67 | 67 | const DEFAULT_BASIC_AUTH = false; |
| 68 | +const DEFAULT_PERUSER_BASIC_AUTH = false; | |
| 68 | 69 | |
| 69 | 70 | const DEFAULT_ENABLE_IPV6 = false; |
| 70 | 71 | const DEFAULT_ENABLE_IPV4 = true; |
| @@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode' | ||
| 184 | 185 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 185 | 186 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); |
| 186 | 187 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); |
| 188 | +const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', DEFAULT_PERUSER_BASIC_AUTH); | |
| 187 | 189 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); |
| 188 | 190 | |
| 189 | 191 | const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY); |
| @@ -756,9 +758,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) { | ||
| 756 | 758 | } |
| 757 | 759 | |
| 758 | 760 | 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 | + } | |
| 762 | 766 | } |
| 763 | 767 | } |
| 764 | 768 | }; |
| @@ -2,14 +2,18 @@ | ||
| 2 | 2 | * When applied, this middleware will ensure the request contains the required header for basic authentication and only |
| 3 | 3 | * allow access to the endpoint after successful authentication. |
| 4 | 4 | */ |
| 5 | 5 | const { 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); | |
| 6 | 10 | |
| 7 | 11 | const unauthorizedResponse = (res) => { |
| 8 | 12 | res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); |
| 9 | 13 | return res.status(401).send('Authentication required'); |
| 10 | 14 | }; |
| 11 | 15 | |
| 12 | 16 | const basicAuthMiddleware = async function (request, response, callback) { |
| 13 | 17 | const config = getConfig(); |
| 14 | 18 | const authHeader = request.headers.authorization; |
| 15 | 19 | |
| @@ -27,11 +31,25 @@ const basicAuthMiddleware = function (request, response, callback) { | ||
| 27 | 31 | .toString('utf8') |
| 28 | 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 | 36 | return callback(); |
| 32 | 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 | + } | |
| 50 | + } | |
| 34 | 51 | } |
| 52 | + return unauthorizedResponse(response); | |
| 35 | 53 | }; |
| 36 | 54 | |
| 37 | 55 | module.exports = basicAuthMiddleware; |
| @@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets'); | ||
| 19 | 19 | const KEY_PREFIX = 'user:'; |
| 20 | 20 | const AVATAR_PREFIX = 'avatar:'; |
| 21 | 21 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); |
| 22 | +const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); | |
| 23 | +const PERUSER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | |
| 22 | 24 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
| 23 | 25 | |
| 24 | 26 | /** |
| @@ -575,6 +577,31 @@ async function tryAutoLogin(request) { | ||
| 575 | 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 | 605 | const userHandles = await getAllUserHandles(); |
| 579 | 606 | if (userHandles.length === 1) { |
| 580 | 607 | const user = await storage.getItem(toKey(userHandles[0])); |
| @@ -583,7 +610,78 @@ async function tryAutoLogin(request) { | ||
| 583 | 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 | +} | |
| 586 | 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 | + } | |
| 684 | + | |
| 587 | 685 | return false; |
| 588 | 686 | } |
| 589 | 687 | |