Flag to disable login UI and endpoints for credentialed auth (#3984)

* Flag to disable login UI and endpoints for credentialed auth

* dev build

* fix translation key
This commit is contained in:
Timothy Carambat
2025-06-11 12:46:40 -07:00
committed by GitHub
parent feadf1f0ff
commit 2055c8accd
14 changed files with 179 additions and 46 deletions

View File

@@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: ['2095-model-swap-in-chat'] # put your current branch to create a build. Core team only.
branches: ['3982-disable-login-simple-sso'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'

View File

@@ -329,6 +329,7 @@ GID='1000'
# Enable simple SSO passthrough to pre-authenticate users from a third party service.
# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.
# SIMPLE_SSO_ENABLED=1
# SIMPLE_SSO_NO_LOGIN=1
# Allow scraping of any IP address in collector - must be string "true" to be enabled
# See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information.

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import System from "@/models/system";
/**
* Checks if Simple SSO is enabled and if the user should be redirected to the SSO login page.
* @returns {{loading: boolean, ssoConfig: {enabled: boolean, noLogin: boolean}}}
*/
export default function useSimpleSSO() {
const [loading, setLoading] = useState(true);
const [ssoConfig, setSsoConfig] = useState({
enabled: false,
noLogin: false,
});
useEffect(() => {
async function checkSsoConfig() {
try {
const settings = await System.keys();
setSsoConfig({
enabled: settings?.SimpleSSOEnabled,
noLogin: settings?.SimpleSSONoLogin,
});
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
checkSsoConfig();
}, []);
return { loading, ssoConfig };
}

View File

@@ -39,9 +39,9 @@ export default function SimpleSSOPassthrough() {
if (error)
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center flex-col gap-4">
<p className="text-white font-mono text-lg">{error}</p>
<p className="text-white/80 font-mono text-sm">
<div className="w-screen h-screen overflow-hidden bg-theme-bg-primary flex items-center justify-center flex-col gap-4">
<p className="text-theme-text-primary font-mono text-lg">{error}</p>
<p className="text-theme-text-secondary font-mono text-sm">
Please contact the system administrator about this error.
</p>
</div>

View File

@@ -4,11 +4,24 @@ import { FullScreenLoader } from "@/components/Preloader";
import { Navigate } from "react-router-dom";
import paths from "@/utils/paths";
import useQuery from "@/hooks/useQuery";
import useSimpleSSO from "@/hooks/useSimpleSSO";
/**
* Login page that handles both single and multi-user login.
*
* If Simple SSO is enabled and no login is allowed, the user will be redirected to the SSO login page
* which may not have a token so the login will fail.
*
* @returns {JSX.Element}
*/
export default function Login() {
const query = useQuery();
const { loading: ssoLoading, ssoConfig } = useSimpleSSO();
const { loading, requiresAuth, mode } = usePasswordModal(!!query.get("nt"));
if (loading) return <FullScreenLoader />;
if (loading || ssoLoading) return <FullScreenLoader />;
if (ssoConfig.enabled && ssoConfig.noLogin)
return <Navigate to={paths.sso.login()} />;
if (requiresAuth === false) return <Navigate to={paths.home()} />;
return <PasswordModal mode={mode} />;

View File

@@ -284,7 +284,7 @@ const MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => {
htmlFor="name"
className="block mb-3 text-sm font-medium text-white"
>
{t("common.adminUsername")}
{t("onboarding.userSetup.adminUsername")}
</label>
<input
name="username"

View File

@@ -18,6 +18,11 @@ export default {
login: (noTry = false) => {
return `/login${noTry ? "?nt=1" : ""}`;
},
sso: {
login: () => {
return "/sso/simple";
},
},
onboarding: {
home: () => {
return "/onboarding";

View File

@@ -326,6 +326,7 @@ TTS_PROVIDER="native"
# Enable simple SSO passthrough to pre-authenticate users from a third party service.
# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.
# SIMPLE_SSO_ENABLED=1
# SIMPLE_SSO_NO_LOGIN=1
# Allow scraping of any IP address in collector - must be string "true" to be enabled
# See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information.

View File

@@ -25,6 +25,9 @@ const {
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const ImportedPlugin = require("../utils/agents/imported");
const {
simpleSSOLoginDisabledMiddleware,
} = require("../utils/middleware/simpleSSOEnabled");
function adminEndpoints(app) {
if (!app) return;
@@ -168,7 +171,11 @@ function adminEndpoints(app) {
app.post(
"/admin/invite/new",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
[
validatedRequest,
strictMultiUserRoleValid([ROLES.admin, ROLES.manager]),
simpleSSOLoginDisabledMiddleware,
],
async (request, response) => {
try {
const user = await userFromSession(request, response);

View File

@@ -2,6 +2,9 @@ const { EventLogs } = require("../models/eventLogs");
const { Invite } = require("../models/invite");
const { User } = require("../models/user");
const { reqBody } = require("../utils/http");
const {
simpleSSOLoginDisabledMiddleware,
} = require("../utils/middleware/simpleSSOEnabled");
function inviteEndpoints(app) {
if (!app) return;
@@ -31,46 +34,50 @@ function inviteEndpoints(app) {
}
});
app.post("/invite/:code", async (request, response) => {
try {
const { code } = request.params;
const { username, password } = reqBody(request);
const invite = await Invite.get({ code });
if (!invite || invite.status !== "pending") {
response
.status(200)
.json({ success: false, error: "Invite not found or is invalid." });
return;
app.post(
"/invite/:code",
[simpleSSOLoginDisabledMiddleware],
async (request, response) => {
try {
const { code } = request.params;
const { username, password } = reqBody(request);
const invite = await Invite.get({ code });
if (!invite || invite.status !== "pending") {
response
.status(200)
.json({ success: false, error: "Invite not found or is invalid." });
return;
}
const { user, error } = await User.create({
username,
password,
role: "default",
});
if (!user) {
console.error("Accepting invite:", error);
response
.status(200)
.json({ success: false, error: "Could not create user." });
return;
}
await Invite.markClaimed(invite.id, user);
await EventLogs.logEvent(
"invite_accepted",
{
username: user.username,
},
user.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
const { user, error } = await User.create({
username,
password,
role: "default",
});
if (!user) {
console.error("Accepting invite:", error);
response
.status(200)
.json({ success: false, error: "Could not create user." });
return;
}
await Invite.markClaimed(invite.id, user);
await EventLogs.logEvent(
"invite_accepted",
{
username: user.username,
},
user.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
);
}
module.exports = { inviteEndpoints };

View File

@@ -54,7 +54,10 @@ const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey");
const {
chatHistoryViewable,
} = require("../utils/middleware/chatHistoryViewable");
const { simpleSSOEnabled } = require("../utils/middleware/simpleSSOEnabled");
const {
simpleSSOEnabled,
simpleSSOLoginDisabled,
} = require("../utils/middleware/simpleSSOEnabled");
const { TemporaryAuthToken } = require("../models/temporaryAuthToken");
const { SystemPromptVariables } = require("../models/systemPromptVariables");
const { VALID_COMMANDS } = require("../utils/chats");
@@ -116,6 +119,17 @@ function systemEndpoints(app) {
const bcrypt = require("bcrypt");
if (await SystemSettings.isMultiUserMode()) {
if (simpleSSOLoginDisabled()) {
response.status(403).json({
user: null,
valid: false,
token: null,
message:
"[005] Login via credentials has been disabled by the administrator.",
});
return;
}
const { username, password } = reqBody(request);
const existingUser = await User._get({ username: String(username) });

View File

@@ -279,6 +279,12 @@ const SystemSettings = {
// Disable View Chat History for the whole instance.
DisableViewChatHistory:
"DISABLE_VIEW_CHAT_HISTORY" in process.env || false,
// --------------------------------------------------------
// Simple SSO Settings
// --------------------------------------------------------
SimpleSSOEnabled: "SIMPLE_SSO_ENABLED" in process.env || false,
SimpleSSONoLogin: "SIMPLE_SSO_NO_LOGIN" in process.env || false,
};
},

View File

@@ -1084,6 +1084,7 @@ function dumpENV() {
"DISABLE_VIEW_CHAT_HISTORY",
// Simple SSO
"SIMPLE_SSO_ENABLED",
"SIMPLE_SSO_NO_LOGIN",
// Community Hub
"COMMUNITY_HUB_BUNDLE_DOWNLOADS_ENABLED",

View File

@@ -34,6 +34,51 @@ async function simpleSSOEnabled(_, response, next) {
next();
}
/**
* Checks if simple SSO login is disabled by checking if the
* SIMPLE_SSO_NO_LOGIN environment variable is set as well as
* SIMPLE_SSO_ENABLED is set.
*
* This check should only be run when in multi-user mode when used.
* @returns {boolean}
*/
function simpleSSOLoginDisabled() {
return (
"SIMPLE_SSO_ENABLED" in process.env && "SIMPLE_SSO_NO_LOGIN" in process.env
);
}
/**
* Middleware that checks if simple SSO login is disabled by checking if the
* SIMPLE_SSO_NO_LOGIN environment variable is set as well as
* SIMPLE_SSO_ENABLED is set.
*
* This middleware will 403 if SSO is enabled and no login is allowed and
* the system is in multi-user mode. Otherwise, it will call next.
*
* @param {import("express").Request} request
* @param {import("express").Response} response
* @param {import("express").NextFunction} next
* @returns {void}
*/
async function simpleSSOLoginDisabledMiddleware(_, response, next) {
if (!("multiUserMode" in response.locals)) {
const multiUserMode = await SystemSettings.isMultiUserMode();
response.locals.multiUserMode = multiUserMode;
}
if (response.locals.multiUserMode && simpleSSOLoginDisabled()) {
response.status(403).json({
success: false,
error: "Login via credentials has been disabled by the administrator.",
});
return;
}
next();
}
module.exports = {
simpleSSOEnabled,
simpleSSOLoginDisabled,
simpleSSOLoginDisabledMiddleware,
};