diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml
index dcd1ba569..18873afe4 100644
--- a/.github/workflows/dev-build.yaml
+++ b/.github/workflows/dev-build.yaml
@@ -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/*'
diff --git a/docker/.env.example b/docker/.env.example
index 755437c2e..9c65405c2 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -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.
diff --git a/frontend/src/hooks/useSimpleSSO.js b/frontend/src/hooks/useSimpleSSO.js
new file mode 100644
index 000000000..35da97520
--- /dev/null
+++ b/frontend/src/hooks/useSimpleSSO.js
@@ -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 };
+}
diff --git a/frontend/src/pages/Login/SSO/simple.jsx b/frontend/src/pages/Login/SSO/simple.jsx
index 48b9edf93..cfef761f9 100644
--- a/frontend/src/pages/Login/SSO/simple.jsx
+++ b/frontend/src/pages/Login/SSO/simple.jsx
@@ -39,9 +39,9 @@ export default function SimpleSSOPassthrough() {
if (error)
return (
-
-
{error}
-
+
+
{error}
+
Please contact the system administrator about this error.
diff --git a/frontend/src/pages/Login/index.jsx b/frontend/src/pages/Login/index.jsx
index 4e77a5c66..0d4c2623c 100644
--- a/frontend/src/pages/Login/index.jsx
+++ b/frontend/src/pages/Login/index.jsx
@@ -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
;
+
+ if (loading || ssoLoading) return
;
+ if (ssoConfig.enabled && ssoConfig.noLogin)
+ return
;
if (requiresAuth === false) return
;
return
;
diff --git a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx
index 536029e82..1c9ad51bb 100644
--- a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx
@@ -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")}
{
return `/login${noTry ? "?nt=1" : ""}`;
},
+ sso: {
+ login: () => {
+ return "/sso/simple";
+ },
+ },
onboarding: {
home: () => {
return "/onboarding";
diff --git a/server/.env.example b/server/.env.example
index ef6292c54..809e36ed8 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -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.
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 211f50465..4964c35a1 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -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);
diff --git a/server/endpoints/invite.js b/server/endpoints/invite.js
index 38eb71de8..9a05ed7cf 100644
--- a/server/endpoints/invite.js
+++ b/server/endpoints/invite.js
@@ -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 };
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 4077844e7..ee3778be0 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -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) });
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index a7a3e752c..bb7311fb8 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -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,
};
},
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index b69c96417..f209ef450 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -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",
diff --git a/server/utils/middleware/simpleSSOEnabled.js b/server/utils/middleware/simpleSSOEnabled.js
index 903200c03..2c6bb7220 100644
--- a/server/utils/middleware/simpleSSOEnabled.js
+++ b/server/utils/middleware/simpleSSOEnabled.js
@@ -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,
};