From e6793bf6eaa14fdad4deed7dca39703f82ba6314 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 01:38:39 +0200 Subject: [PATCH] Add optional auto-redirect to SSO on the login page Sites that delegate all sign-in to an OIDC provider had no way to skip the password form. New sso_auto_redirect setting (admin > Settings > User session) makes /session forward straight to /session/sso via location.replace so the back button cannot trap users in a redirect loop. ?nosso=1 and an existing sso_error keep the password form reachable for admin recovery; OAuth app-consent flows are unaffected. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- frontend/public/locales/en-US/dashboard.json | 2 ++ frontend/public/locales/zh-CN/dashboard.json | 2 ++ frontend/src/api/site.ts | 1 + .../src/component/Admin/Settings/Settings.tsx | 1 + .../Settings/UserSession/SSOSettings.tsx | 22 +++++++++++++++++++ .../component/Pages/Login/Signin/SignIn.tsx | 15 ++++++++++++- inventory/setting.go | 1 + pkg/setting/provider.go | 1 + pkg/setting/types.go | 1 + service/basic/site.go | 2 ++ 10 files changed, 47 insertions(+), 1 deletion(-) diff --git a/frontend/public/locales/en-US/dashboard.json b/frontend/public/locales/en-US/dashboard.json index 43356922..ef5473ab 100644 --- a/frontend/public/locales/en-US/dashboard.json +++ b/frontend/public/locales/en-US/dashboard.json @@ -840,6 +840,8 @@ "ssoIssuerDes": "Base URL of the OIDC issuer, e.g. <0>https://keycloak.example.com/realms/master. The provider metadata is fetched from <1>.well-known/openid-configuration under this URL.", "ssoRegisterEnabled": "Allow automatic registration", "ssoRegisterEnabledDes": "Automatically create a local account when a user signs in via SSO for the first time. The sign-up email filter below also applies.", + "ssoAutoRedirect": "Auto redirect to SSO", + "ssoAutoRedirectDes": "Skip the login form and send visitors straight to the identity provider. Append <0>?nosso=1 to the login URL to reach the password form (e.g. for admin recovery).", "ssoCallbackUrl": "Callback URL", "ssoCallbackUrlDes": "Register this URL as the redirect/callback URL in your identity provider: <0>{{url}}", "themeVisible": "Visible", diff --git a/frontend/public/locales/zh-CN/dashboard.json b/frontend/public/locales/zh-CN/dashboard.json index 944d5352..755dfbcd 100644 --- a/frontend/public/locales/zh-CN/dashboard.json +++ b/frontend/public/locales/zh-CN/dashboard.json @@ -840,6 +840,8 @@ "ssoIssuerDes": "OIDC Issuer 的基础 URL,例如 <0>https://keycloak.example.com/realms/master。将从该地址下的 <1>.well-known/openid-configuration 获取提供方元数据。", "ssoRegisterEnabled": "允许自动注册", "ssoRegisterEnabledDes": "用户首次通过 SSO 登录时自动创建本地账号。下方的注册邮箱过滤规则同样适用。", + "ssoAutoRedirect": "自动跳转至 SSO", + "ssoAutoRedirectDes": "跳过登录表单,直接跳转至身份提供商。在登录地址后附加 <0>?nosso=1 可进入密码登录表单(例如管理员账户恢复)。", "ssoCallbackUrl": "回调地址", "ssoCallbackUrlDes": "请在身份提供方中将此 URL 注册为 Redirect/Callback URL:<0>{{url}}", "themeVisible": "可见", diff --git a/frontend/src/api/site.ts b/frontend/src/api/site.ts index 2103a7cd..67ca5773 100644 --- a/frontend/src/api/site.ts +++ b/frontend/src/api/site.ts @@ -30,6 +30,7 @@ export interface SiteConfig { register_enabled?: boolean; sso_enabled?: boolean; sso_display_name?: string; + sso_auto_redirect?: boolean; logo?: string; logo_light?: string; tos_url?: string; diff --git a/frontend/src/component/Admin/Settings/Settings.tsx b/frontend/src/component/Admin/Settings/Settings.tsx index c0b8a234..bee58c83 100644 --- a/frontend/src/component/Admin/Settings/Settings.tsx +++ b/frontend/src/component/Admin/Settings/Settings.tsx @@ -198,6 +198,7 @@ const Settings = () => { "sso_client_secret", "sso_scopes", "sso_register_enabled", + "sso_auto_redirect", "email_filter_mode", "email_filter_list", "email_disable_subaddress", diff --git a/frontend/src/component/Admin/Settings/UserSession/SSOSettings.tsx b/frontend/src/component/Admin/Settings/UserSession/SSOSettings.tsx index 057fb3a1..de41f106 100644 --- a/frontend/src/component/Admin/Settings/UserSession/SSOSettings.tsx +++ b/frontend/src/component/Admin/Settings/UserSession/SSOSettings.tsx @@ -141,6 +141,28 @@ const SSOSettings = () => { /> {t("settings.ssoRegisterEnabledDes")} + + + setSettings({ + sso_auto_redirect: e.target.checked ? "1" : "0", + }) + } + /> + } + label={{t("settings.ssoAutoRedirect")}} + /> + + ]} + /> + + diff --git a/frontend/src/component/Pages/Login/Signin/SignIn.tsx b/frontend/src/component/Pages/Login/Signin/SignIn.tsx index cfb05f55..078c6b64 100644 --- a/frontend/src/component/Pages/Login/Signin/SignIn.tsx +++ b/frontend/src/component/Pages/Login/Signin/SignIn.tsx @@ -14,7 +14,7 @@ import { sendPrepareLogin, sendResetEmail, } from "../../../../api/api.ts"; -import { AppError, Code } from "../../../../api/request.ts"; +import { ApiPrefix, AppError, Code } from "../../../../api/request.ts"; import { AppRegistration, GrantService, LoginResponse, PrepareLoginResponse } from "../../../../api/user.ts"; import { clearOAuthApp, setOAuthApp, setOAuthAppLoading } from "../../../../redux/globalStateSlice.ts"; import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; @@ -86,6 +86,7 @@ const EmailLogin = ({ oauthConsent }: SignInProps) => { // Get OAuth app from Redux const app = useAppSelector((state) => state.globalState.oauthApp); + const { sso_enabled, sso_auto_redirect } = useAppSelector((state) => state.siteConfig.login.config); const [phase, setPhase] = useState(EmailLoginPhase.CollectEmail); const [email, setEmail] = useState(""); @@ -312,6 +313,18 @@ const EmailLogin = ({ oauthConsent }: SignInProps) => { }); } + // Auto-redirect to the configured SSO provider. `nosso` and an existing + // sso_error keep the password form reachable for admin recovery. + if (sso_enabled && sso_auto_redirect && !ssoError && !query.get("nosso") && !isOAuthFlow) { + const target = new URL(ApiPrefix + "/session/sso", window.location.origin); + const redirect = query.get("redirect"); + if (redirect) { + target.searchParams.set("redirect", redirect); + } + window.location.replace(target.toString()); + return; + } + const init = async () => { if (isOAuthFlow) { const registration = await loadAppRegistration(); diff --git a/inventory/setting.go b/inventory/setting.go index 064a6d2d..fdc5e678 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -558,6 +558,7 @@ var DefaultSettings = map[string]string{ "sso_client_secret": "", "sso_scopes": "", "sso_register_enabled": "1", + "sso_auto_redirect": "0", "email_filter_mode": "0", "email_filter_list": "", "email_disable_subaddress": "0", diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index ed6b3c46..3a169ce0 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -908,6 +908,7 @@ func (s *settingProvider) SSO(ctx context.Context) *SSO { ClientSecret: s.getString(ctx, "sso_client_secret", ""), Scopes: strings.Join(scopeList, " "), RegisterEnabled: s.getBoolean(ctx, "sso_register_enabled", true), + AutoRedirect: s.getBoolean(ctx, "sso_auto_redirect", false), } } diff --git a/pkg/setting/types.go b/pkg/setting/types.go index d135f509..32fd7730 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -82,6 +82,7 @@ type SSO struct { ClientSecret string Scopes string RegisterEnabled bool + AutoRedirect bool } type EmailFilterMode int diff --git a/service/basic/site.go b/service/basic/site.go index 3e1f5cb8..7e12babb 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -47,6 +47,7 @@ type SiteConfig struct { PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` SSOEnabled bool `json:"sso_enabled,omitempty"` SSODisplayName string `json:"sso_display_name,omitempty"` + SSOAutoRedirect bool `json:"sso_auto_redirect,omitempty"` // Explorer section Icons string `json:"icons,omitempty"` @@ -106,6 +107,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { TosUrl: legalDocs.TermsOfService, SSOEnabled: sso.Enabled && sso.Issuer != "" && sso.ClientID != "", SSODisplayName: sso.DisplayName, + SSOAutoRedirect: sso.AutoRedirect, }, nil case "explorer": explorerSettings := settings.ExplorerFrontendSettings(c)