mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #2953 from QuantumEntangledAndy/feat/AdditionalLogins
Add additional login methods
This commit is contained in:
@@ -51,6 +51,19 @@ requestProxy:
|
|||||||
enableUserAccounts: false
|
enableUserAccounts: false
|
||||||
# Enable discreet login mode: hides user list on the login screen
|
# Enable discreet login mode: hides user list on the login screen
|
||||||
enableDiscreetLogin: false
|
enableDiscreetLogin: false
|
||||||
|
# Enable's authlia based auto login. Only enable this if you
|
||||||
|
# have setup and installed Authelia as a middle-ware on your
|
||||||
|
# reverse proxy
|
||||||
|
# https://www.authelia.com/
|
||||||
|
# This will use auto login to an account with the same username
|
||||||
|
# as that used for authlia. (Ensure the username in authlia
|
||||||
|
# is an exact match with that in sillytavern)
|
||||||
|
autheliaAuth: false
|
||||||
|
# If `basicAuthMode` and this are enabled then
|
||||||
|
# the username and passwords for basic auth are the same as those
|
||||||
|
# for the individual accounts
|
||||||
|
perUserBasicAuth: false
|
||||||
|
|
||||||
# User session timeout *in seconds* (defaults to 24 hours).
|
# User session timeout *in seconds* (defaults to 24 hours).
|
||||||
## Set to a positive number to expire session after a certain time of inactivity
|
## Set to a positive number to expire session after a certain time of inactivity
|
||||||
## Set to 0 to expire session when the browser is closed
|
## Set to 0 to expire session when the browser is closed
|
||||||
|
@@ -180,7 +180,13 @@ function displayError(message) {
|
|||||||
* Preserves the query string.
|
* Preserves the query string.
|
||||||
*/
|
*/
|
||||||
function redirectToHome() {
|
function redirectToHome() {
|
||||||
window.location.href = '/' + window.location.search;
|
// After a login theres no need to preserve the
|
||||||
|
// noauto (if present)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
urlParams.delete('noauto');
|
||||||
|
|
||||||
|
window.location.href = '/' + urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -848,7 +848,14 @@ async function logout() {
|
|||||||
headers: getRequestHeaders(),
|
headers: getRequestHeaders(),
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.reload();
|
// On an explicit logout stop auto login
|
||||||
|
// to allow user to change username even
|
||||||
|
// when auto auth (such as authelia or basic)
|
||||||
|
// would be valid
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set('noauto', 'true');
|
||||||
|
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -65,6 +65,7 @@ const DEFAULT_WHITELIST = true;
|
|||||||
const DEFAULT_ACCOUNTS = false;
|
const DEFAULT_ACCOUNTS = false;
|
||||||
const DEFAULT_CSRF_DISABLED = false;
|
const DEFAULT_CSRF_DISABLED = false;
|
||||||
const DEFAULT_BASIC_AUTH = false;
|
const DEFAULT_BASIC_AUTH = false;
|
||||||
|
const DEFAULT_PER_USER_BASIC_AUTH = false;
|
||||||
|
|
||||||
const DEFAULT_ENABLE_IPV6 = false;
|
const DEFAULT_ENABLE_IPV6 = false;
|
||||||
const DEFAULT_ENABLE_IPV4 = true;
|
const DEFAULT_ENABLE_IPV4 = true;
|
||||||
@@ -184,6 +185,7 @@ const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode'
|
|||||||
const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
|
const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
|
||||||
const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
|
const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED);
|
||||||
const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
|
const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
|
||||||
|
const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
|
||||||
const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
|
const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
|
||||||
|
|
||||||
const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
|
const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
|
||||||
@@ -361,7 +363,7 @@ app.get('/login', async (request, response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const autoLogin = await userModule.tryAutoLogin(request);
|
const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode);
|
||||||
|
|
||||||
if (autoLogin) {
|
if (autoLogin) {
|
||||||
return response.redirect('/');
|
return response.redirect('/');
|
||||||
@@ -756,11 +758,15 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (basicAuthMode) {
|
if (basicAuthMode) {
|
||||||
|
if (perUserBasicAuth && !enableAccounts) {
|
||||||
|
console.error(color.red('Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.'));
|
||||||
|
} else if (!perUserBasicAuth) {
|
||||||
const basicAuthUser = getConfigValue('basicAuthUser', {});
|
const basicAuthUser = getConfigValue('basicAuthUser', {});
|
||||||
if (!basicAuthUser?.username || !basicAuthUser?.password) {
|
if (!basicAuthUser?.username || !basicAuthUser?.password) {
|
||||||
console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
|
console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,14 +2,19 @@
|
|||||||
* When applied, this middleware will ensure the request contains the required header for basic authentication and only
|
* When applied, this middleware will ensure the request contains the required header for basic authentication and only
|
||||||
* allow access to the endpoint after successful authentication.
|
* allow access to the endpoint after successful authentication.
|
||||||
*/
|
*/
|
||||||
const { getConfig } = require('../util.js');
|
const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js');
|
||||||
|
const { getConfig, getConfigValue } = require('../util.js');
|
||||||
|
const storage = require('node-persist');
|
||||||
|
|
||||||
|
const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
|
||||||
|
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||||
|
|
||||||
const unauthorizedResponse = (res) => {
|
const unauthorizedResponse = (res) => {
|
||||||
res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
|
res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
|
||||||
return res.status(401).send('Authentication required');
|
return res.status(401).send('Authentication required');
|
||||||
};
|
};
|
||||||
|
|
||||||
const basicAuthMiddleware = function (request, response, callback) {
|
const basicAuthMiddleware = async function (request, response, callback) {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const authHeader = request.headers.authorization;
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
@@ -23,15 +28,25 @@ const basicAuthMiddleware = function (request, response, callback) {
|
|||||||
return unauthorizedResponse(response);
|
return unauthorizedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS;
|
||||||
const [username, password] = Buffer.from(credentials, 'base64')
|
const [username, password] = Buffer.from(credentials, 'base64')
|
||||||
.toString('utf8')
|
.toString('utf8')
|
||||||
.split(':');
|
.split(':');
|
||||||
|
|
||||||
if (username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
|
if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) {
|
||||||
|
return callback();
|
||||||
|
} else if (usePerUserAuth) {
|
||||||
|
const userHandles = await getAllUserHandles();
|
||||||
|
for (const userHandle of userHandles) {
|
||||||
|
if (username === userHandle) {
|
||||||
|
const user = await storage.getItem(toKey(userHandle));
|
||||||
|
if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) {
|
||||||
return callback();
|
return callback();
|
||||||
} else {
|
|
||||||
return unauthorizedResponse(response);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unauthorizedResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = basicAuthMiddleware;
|
module.exports = basicAuthMiddleware;
|
||||||
|
104
src/users.js
104
src/users.js
@@ -19,6 +19,8 @@ const { readSecret, writeSecret } = require('./endpoints/secrets');
|
|||||||
const KEY_PREFIX = 'user:';
|
const KEY_PREFIX = 'user:';
|
||||||
const AVATAR_PREFIX = 'avatar:';
|
const AVATAR_PREFIX = 'avatar:';
|
||||||
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||||
|
const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);
|
||||||
|
const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
|
||||||
const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
|
const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -565,13 +567,42 @@ function shouldRedirectToLogin(request) {
|
|||||||
return ENABLE_ACCOUNTS && !request.user;
|
return ENABLE_ACCOUNTS && !request.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries auto-login if there is only one user and it's not password protected.
|
||||||
|
* or another configured method such authlia or basic
|
||||||
|
* @param {import('express').Request} request Request object
|
||||||
|
* @param {boolean} basicAuthMode If Basic auth mode is enabled
|
||||||
|
* @returns {Promise<boolean>} Whether auto-login was performed
|
||||||
|
*/
|
||||||
|
async function tryAutoLogin(request, basicAuthMode) {
|
||||||
|
if (!ENABLE_ACCOUNTS || request.user || !request.session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.query.noauto) {
|
||||||
|
if (await singleUserLogin(request)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AUTHELIA_AUTH && await autheliaUserLogin(request)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries auto-login if there is only one user and it's not password protected.
|
* Tries auto-login if there is only one user and it's not password protected.
|
||||||
* @param {import('express').Request} request Request object
|
* @param {import('express').Request} request Request object
|
||||||
* @returns {Promise<boolean>} Whether auto-login was performed
|
* @returns {Promise<boolean>} Whether auto-login was performed
|
||||||
*/
|
*/
|
||||||
async function tryAutoLogin(request) {
|
async function singleUserLogin(request) {
|
||||||
if (!ENABLE_ACCOUNTS || request.user || !request.session) {
|
if (!request.session) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,6 +614,75 @@ async function tryAutoLogin(request) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries auto-login with authlia trusted headers.
|
||||||
|
* https://www.authelia.com/integration/trusted-header-sso/introduction/
|
||||||
|
* @param {import('express').Request} request Request object
|
||||||
|
* @returns {Promise<boolean>} Whether auto-login was performed
|
||||||
|
*/
|
||||||
|
async function autheliaUserLogin(request) {
|
||||||
|
if (!request.session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteUser = request.get('Remote-User');
|
||||||
|
if (!remoteUser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userHandles = await getAllUserHandles();
|
||||||
|
for (const userHandle of userHandles) {
|
||||||
|
if (remoteUser === userHandle) {
|
||||||
|
const user = await storage.getItem(toKey(userHandle));
|
||||||
|
if (user && user.enabled) {
|
||||||
|
request.session.handle = userHandle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries auto-login with basic auth username.
|
||||||
|
* @param {import('express').Request} request Request object
|
||||||
|
* @returns {Promise<boolean>} Whether auto-login was performed
|
||||||
|
*/
|
||||||
|
async function basicUserLogin(request) {
|
||||||
|
if (!request.session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [scheme, credentials] = authHeader.split(' ');
|
||||||
|
|
||||||
|
if (scheme !== 'Basic' || !credentials) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [username, password] = Buffer.from(credentials, 'base64')
|
||||||
|
.toString('utf8')
|
||||||
|
.split(':');
|
||||||
|
|
||||||
|
const userHandles = await getAllUserHandles();
|
||||||
|
for (const userHandle of userHandles) {
|
||||||
|
if (username === userHandle) {
|
||||||
|
const user = await storage.getItem(toKey(userHandle));
|
||||||
|
// Verify pass again here just to be sure
|
||||||
|
if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) {
|
||||||
|
request.session.handle = userHandle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user