From 184388879d71b03c29f35ec692ff6e3740cc20f8 Mon Sep 17 00:00:00 2001 From: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:06:03 +0500 Subject: [PATCH] feat: Add `disable_password_login` configuration to support SSO-only authentication (#13151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What problem does this PR solve? Enterprise deployments that use an external Identity Provider (e.g., Microsoft Entra ID, Okta, Keycloak) need the ability to enforce SSO-only authentication by hiding the email/password login form. Currently, the login page always shows the password form alongside OAuth buttons, with no way to disable it. This PR adds a `disable_password_login` configuration option under the existing `authentication` section in `service_conf.yaml`. When set to `true`, the login page only displays configured OAuth/SSO buttons and hides the email/password form, "Remember me" checkbox, and "Sign up" link. The flag can be set via: - `service_conf.yaml` (`authentication.disable_password_login: true`) - Environment variable (`DISABLE_PASSWORD_LOGIN=true`) Default behavior is unchanged (`false`). ### Behavior | `disable_password_login` | OAuth configured | Result | |---|---|---| | `false` (default) | No | Standard email/password form | | `false` | Yes | Email/password form + SSO buttons below | | `true` | Yes | **SSO buttons only** (no form, no sign up link) | | `true` | No | Empty card (admin should configure OAuth first) | ### Type of change - [x] New Feature (non-breaking change which adds functionality) ### Files changed (5) 1. `docker/service_conf.yaml.template` — added `disable_password_login: false` under authentication 2. `common/settings.py` — added `DISABLE_PASSWORD_LOGIN` global variable and loader in `init_settings()` 3. `common/config_utils.py` — fixed `TypeError` in `show_configs()` when authentication section contains non-dict values (e.g., booleans) 4. `api/apps/system_app.py` — exposed `disablePasswordLogin` flag in `/config` endpoint 5. `web/src/pages/login/index.tsx` — conditionally render password form based on config flag; OAuth buttons always render when channels exist --------- Co-authored-by: Ahmad Intisar --- api/apps/system_app.py | 5 +- common/config_utils.py | 4 +- common/settings.py | 13 ++++ docker/.env | 5 +- docker/service_conf.yaml.template | 13 ++-- .../test_system_routes_unit.py | 1 + web/src/pages/login-next/index.tsx | 63 ++++++++++--------- 7 files changed, 63 insertions(+), 41 deletions(-) diff --git a/api/apps/system_app.py b/api/apps/system_app.py index b15054490b..da634ee7f0 100644 --- a/api/apps/system_app.py +++ b/api/apps/system_app.py @@ -371,4 +371,7 @@ def get_config(): type: integer 0 means disabled, 1 means enabled description: Whether user registration is enabled """ - return get_json_result(data={"registerEnabled": settings.REGISTER_ENABLED}) + return get_json_result(data={ + "registerEnabled": settings.REGISTER_ENABLED, + "disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN, + }) diff --git a/common/config_utils.py b/common/config_utils.py index ac55f7e972..d367536de1 100644 --- a/common/config_utils.py +++ b/common/config_utils.py @@ -102,7 +102,7 @@ def show_configs(): if "authentication" in k: v = copy.deepcopy(v) for key, val in v.items(): - if "http_secret_key" in val: + if isinstance(val, dict) and "http_secret_key" in val: val["http_secret_key"] = "*" * 8 msg += f"\n\t{k}: {v}" logging.info(msg) @@ -152,4 +152,4 @@ def update_config(key, value, conf_name=SERVICE_CONF): with FileLock(os.path.join(os.path.dirname(conf_path), ".lock")): config = load_yaml_conf(conf_path=conf_path) or {} config[key] = value - rewrite_yaml_conf(conf_path=conf_path, config=config) + rewrite_yaml_conf(conf_path=conf_path, config=config) \ No newline at end of file diff --git a/common/settings.py b/common/settings.py index de26353637..fe3d07b33c 100644 --- a/common/settings.py +++ b/common/settings.py @@ -92,6 +92,8 @@ kg_retriever = None # user registration switch REGISTER_ENABLED = 1 +# SSO-only mode: hide password login form +DISABLE_PASSWORD_LOGIN = False # sandbox-executor-manager SANDBOX_HOST = None @@ -186,6 +188,17 @@ def init_settings(): except Exception: pass + global DISABLE_PASSWORD_LOGIN + try: + env_val = os.environ.get("DISABLE_PASSWORD_LOGIN", "").lower() + if env_val in ("1", "true", "yes"): + DISABLE_PASSWORD_LOGIN = True + else: + authentication_conf = get_base_config("authentication", {}) + DISABLE_PASSWORD_LOGIN = bool(authentication_conf.get("disable_password_login", False)) + except Exception: + pass + global FACTORY_LLM_INFOS try: with open(os.path.join(get_project_base_directory(), "conf", "llm_factories.json"), "r") as f: diff --git a/docker/.env b/docker/.env index 57a42ac1ab..8e20e257bb 100644 --- a/docker/.env +++ b/docker/.env @@ -276,4 +276,7 @@ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # Used for ThreadPoolExecutor -THREAD_POOL_MAX_WORKERS=128 \ No newline at end of file +THREAD_POOL_MAX_WORKERS=128 + +#Option to disable login form for SSO +DISABLE_PASSWORD_LOGIN=false \ No newline at end of file diff --git a/docker/service_conf.yaml.template b/docker/service_conf.yaml.template index 4fac37f55d..ba30a0240d 100644 --- a/docker/service_conf.yaml.template +++ b/docker/service_conf.yaml.template @@ -143,12 +143,13 @@ user_default_llm: # client_secret: "your_client_secret" # redirect_uri: "https://your-app.com/v1/user/oauth/callback/github" # authentication: -# client: -# switch: false -# http_app_key: -# http_secret_key: -# site: -# switch: false +# client: +# switch: false +# http_app_key: +# http_secret_key: +# site: +# switch: false +# disable_password_login: false # permission: # switch: false # component: false diff --git a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py index 08e24992d9..a00ecdbb6f 100644 --- a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py +++ b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py @@ -76,6 +76,7 @@ def _load_system_module(monkeypatch): settings_mod.STORAGE_IMPL_TYPE = "MINIO" settings_mod.DATABASE_TYPE = "MYSQL" settings_mod.REGISTER_ENABLED = True + settings_mod.DISABLE_PASSWORD_LOGIN = False common_pkg.settings = settings_mod monkeypatch.setitem(sys.modules, "common.settings", settings_mod) diff --git a/web/src/pages/login-next/index.tsx b/web/src/pages/login-next/index.tsx index 0384296b36..69c929b89c 100644 --- a/web/src/pages/login-next/index.tsx +++ b/web/src/pages/login-next/index.tsx @@ -43,6 +43,7 @@ type LoginFormContentProps = { channels: { channel: string; icon?: string; display_name: string }[]; handleLoginWithChannel: (channel: string) => void; t: ReturnType['t']; + disablePasswordLogin?: boolean; }; function LoginFormContent({ @@ -56,6 +57,7 @@ function LoginFormContent({ channels, handleLoginWithChannel, t, + disablePasswordLogin, }: LoginFormContentProps) { const face = useContext(FlipFaceContext); const isActiveFace = isLoginPage ? face === 'front' : face === 'back'; @@ -68,6 +70,7 @@ function LoginFormContent({
+ {!disablePasswordLogin && (
{title === 'login' ? t('login') : t('continue')} - {title === 'login' && channels && channels.length > 0 && ( -
- {channels.map((item) => ( - - ))} -
- )}
+ )} - {title === 'login' && registerEnabled && ( + {title === 'login' && channels && channels.length > 0 && ( +
+ {channels.map((item) => ( + + ))} +
+ )} + + {!disablePasswordLogin && title === 'login' && registerEnabled && (

{t('signInTip')} @@ -217,7 +223,7 @@ function LoginFormContent({

)} - {title === 'register' && ( + {!disablePasswordLogin && title === 'register' && (

{t('signUpTip')} @@ -369,14 +375,8 @@ const Login = () => {

{t('title')}

- {/* border border-accent-primary rounded-full */} - {/*
- {t('start')} -
*/}
- {/* Logo and Header */} - {/* Login Form */} { channels={channels || []} handleLoginWithChannel={handleLoginWithChannel} t={t} + disablePasswordLogin={!!config?.disablePasswordLogin} />
@@ -398,4 +399,4 @@ const Login = () => { ); }; -export default Login; +export default Login; \ No newline at end of file