feat: Add disable_password_login configuration to support SSO-only authentication (#13151)

### 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 <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
This commit is contained in:
Ahmad Intisar
2026-03-02 11:06:03 +05:00
committed by GitHub
parent daec36e935
commit 184388879d
7 changed files with 63 additions and 41 deletions

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -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:

View File

@@ -276,4 +276,7 @@ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
# Used for ThreadPoolExecutor
THREAD_POOL_MAX_WORKERS=128
THREAD_POOL_MAX_WORKERS=128
#Option to disable login form for SSO
DISABLE_PASSWORD_LOGIN=false

View File

@@ -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

View File

@@ -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)

View File

@@ -43,6 +43,7 @@ type LoginFormContentProps = {
channels: { channel: string; icon?: string; display_name: string }[];
handleLoginWithChannel: (channel: string) => void;
t: ReturnType<typeof useTranslation>['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({
</h2>
</div>
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
{!disablePasswordLogin && (
<Form {...form}>
<form
className="flex flex-col gap-8 text-text-primary "
@@ -177,32 +180,35 @@ function LoginFormContent({
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
</form>
</Form>
)}
{title === 'login' && registerEnabled && (
{title === 'login' && channels && channels.length > 0 && (
<div className={disablePasswordLogin ? 'py-8' : 'mt-3 border'}>
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
className={disablePasswordLogin ? 'w-full' : ''}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
{!disablePasswordLogin && title === 'login' && registerEnabled && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
@@ -217,7 +223,7 @@ function LoginFormContent({
</p>
</div>
)}
{title === 'register' && (
{!disablePasswordLogin && title === 'register' && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
@@ -369,14 +375,8 @@ const Login = () => {
<h1 className="text-[36px] font-medium text-center mb-2">
{t('title')}
</h1>
{/* border border-accent-primary rounded-full */}
{/* <div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
{t('start')}
</div> */}
</div>
<div className="relative z-10 flex flex-col items-center justify-center min-h-[1050px] px-4 sm:px-6 lg:px-8">
{/* Logo and Header */}
{/* Login Form */}
<FlipCard3D isLoginPage={isLoginPage}>
<LoginFormContent
@@ -390,6 +390,7 @@ const Login = () => {
channels={channels || []}
handleLoginWithChannel={handleLoginWithChannel}
t={t}
disablePasswordLogin={!!config?.disablePasswordLogin}
/>
</FlipCard3D>
</div>
@@ -398,4 +399,4 @@ const Login = () => {
);
};
export default Login;
export default Login;