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

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