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>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 09031da204
commit e6793bf6ea

@ -840,6 +840,8 @@
"ssoIssuerDes": "Base URL of the OIDC issuer, e.g. <0>https://keycloak.example.com/realms/master</0>. The provider metadata is fetched from <1>.well-known/openid-configuration</1> under this URL.", "ssoIssuerDes": "Base URL of the OIDC issuer, e.g. <0>https://keycloak.example.com/realms/master</0>. The provider metadata is fetched from <1>.well-known/openid-configuration</1> under this URL.",
"ssoRegisterEnabled": "Allow automatic registration", "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.", "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</0> to the login URL to reach the password form (e.g. for admin recovery).",
"ssoCallbackUrl": "Callback URL", "ssoCallbackUrl": "Callback URL",
"ssoCallbackUrlDes": "Register this URL as the redirect/callback URL in your identity provider: <0>{{url}}</0>", "ssoCallbackUrlDes": "Register this URL as the redirect/callback URL in your identity provider: <0>{{url}}</0>",
"themeVisible": "Visible", "themeVisible": "Visible",

@ -840,6 +840,8 @@
"ssoIssuerDes": "OIDC Issuer 的基础 URL,例如 <0>https://keycloak.example.com/realms/master</0>。将从该地址下的 <1>.well-known/openid-configuration</1> 获取提供方元数据。", "ssoIssuerDes": "OIDC Issuer 的基础 URL,例如 <0>https://keycloak.example.com/realms/master</0>。将从该地址下的 <1>.well-known/openid-configuration</1> 获取提供方元数据。",
"ssoRegisterEnabled": "允许自动注册", "ssoRegisterEnabled": "允许自动注册",
"ssoRegisterEnabledDes": "用户首次通过 SSO 登录时自动创建本地账号。下方的注册邮箱过滤规则同样适用。", "ssoRegisterEnabledDes": "用户首次通过 SSO 登录时自动创建本地账号。下方的注册邮箱过滤规则同样适用。",
"ssoAutoRedirect": "自动跳转至 SSO",
"ssoAutoRedirectDes": "跳过登录表单,直接跳转至身份提供商。在登录地址后附加 <0>?nosso=1</0> 可进入密码登录表单(例如管理员账户恢复)。",
"ssoCallbackUrl": "回调地址", "ssoCallbackUrl": "回调地址",
"ssoCallbackUrlDes": "请在身份提供方中将此 URL 注册为 Redirect/Callback URL:<0>{{url}}</0>", "ssoCallbackUrlDes": "请在身份提供方中将此 URL 注册为 Redirect/Callback URL:<0>{{url}}</0>",
"themeVisible": "可见", "themeVisible": "可见",

@ -30,6 +30,7 @@ export interface SiteConfig {
register_enabled?: boolean; register_enabled?: boolean;
sso_enabled?: boolean; sso_enabled?: boolean;
sso_display_name?: string; sso_display_name?: string;
sso_auto_redirect?: boolean;
logo?: string; logo?: string;
logo_light?: string; logo_light?: string;
tos_url?: string; tos_url?: string;

@ -198,6 +198,7 @@ const Settings = () => {
"sso_client_secret", "sso_client_secret",
"sso_scopes", "sso_scopes",
"sso_register_enabled", "sso_register_enabled",
"sso_auto_redirect",
"email_filter_mode", "email_filter_mode",
"email_filter_list", "email_filter_list",
"email_disable_subaddress", "email_disable_subaddress",

@ -141,6 +141,28 @@ const SSOSettings = () => {
/> />
<NoMarginHelperText>{t("settings.ssoRegisterEnabledDes")}</NoMarginHelperText> <NoMarginHelperText>{t("settings.ssoRegisterEnabledDes")}</NoMarginHelperText>
</FormControl> </FormControl>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={isTrueVal(values.sso_auto_redirect)}
onChange={(e) =>
setSettings({
sso_auto_redirect: e.target.checked ? "1" : "0",
})
}
/>
}
label={<Typography variant="body2">{t("settings.ssoAutoRedirect")}</Typography>}
/>
<NoMarginHelperText>
<Trans
i18nKey="settings.ssoAutoRedirectDes"
ns="dashboard"
components={[<Code key="0" />]}
/>
</NoMarginHelperText>
</FormControl>
</SettingSectionContent> </SettingSectionContent>
</AccordionDetails> </AccordionDetails>
</StyledAccordion> </StyledAccordion>

@ -14,7 +14,7 @@ import {
sendPrepareLogin, sendPrepareLogin,
sendResetEmail, sendResetEmail,
} from "../../../../api/api.ts"; } 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 { AppRegistration, GrantService, LoginResponse, PrepareLoginResponse } from "../../../../api/user.ts";
import { clearOAuthApp, setOAuthApp, setOAuthAppLoading } from "../../../../redux/globalStateSlice.ts"; import { clearOAuthApp, setOAuthApp, setOAuthAppLoading } from "../../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts";
@ -86,6 +86,7 @@ const EmailLogin = ({ oauthConsent }: SignInProps) => {
// Get OAuth app from Redux // Get OAuth app from Redux
const app = useAppSelector((state) => state.globalState.oauthApp); const app = useAppSelector((state) => state.globalState.oauthApp);
const { sso_enabled, sso_auto_redirect } = useAppSelector((state) => state.siteConfig.login.config);
const [phase, setPhase] = useState<EmailLoginPhase>(EmailLoginPhase.CollectEmail); const [phase, setPhase] = useState<EmailLoginPhase>(EmailLoginPhase.CollectEmail);
const [email, setEmail] = useState(""); 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 () => { const init = async () => {
if (isOAuthFlow) { if (isOAuthFlow) {
const registration = await loadAppRegistration(); const registration = await loadAppRegistration();

@ -558,6 +558,7 @@ var DefaultSettings = map[string]string{
"sso_client_secret": "", "sso_client_secret": "",
"sso_scopes": "", "sso_scopes": "",
"sso_register_enabled": "1", "sso_register_enabled": "1",
"sso_auto_redirect": "0",
"email_filter_mode": "0", "email_filter_mode": "0",
"email_filter_list": "", "email_filter_list": "",
"email_disable_subaddress": "0", "email_disable_subaddress": "0",

@ -908,6 +908,7 @@ func (s *settingProvider) SSO(ctx context.Context) *SSO {
ClientSecret: s.getString(ctx, "sso_client_secret", ""), ClientSecret: s.getString(ctx, "sso_client_secret", ""),
Scopes: strings.Join(scopeList, " "), Scopes: strings.Join(scopeList, " "),
RegisterEnabled: s.getBoolean(ctx, "sso_register_enabled", true), RegisterEnabled: s.getBoolean(ctx, "sso_register_enabled", true),
AutoRedirect: s.getBoolean(ctx, "sso_auto_redirect", false),
} }
} }

@ -82,6 +82,7 @@ type SSO struct {
ClientSecret string ClientSecret string
Scopes string Scopes string
RegisterEnabled bool RegisterEnabled bool
AutoRedirect bool
} }
type EmailFilterMode int type EmailFilterMode int

@ -47,6 +47,7 @@ type SiteConfig struct {
PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"`
SSOEnabled bool `json:"sso_enabled,omitempty"` SSOEnabled bool `json:"sso_enabled,omitempty"`
SSODisplayName string `json:"sso_display_name,omitempty"` SSODisplayName string `json:"sso_display_name,omitempty"`
SSOAutoRedirect bool `json:"sso_auto_redirect,omitempty"`
// Explorer section // Explorer section
Icons string `json:"icons,omitempty"` Icons string `json:"icons,omitempty"`
@ -106,6 +107,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
TosUrl: legalDocs.TermsOfService, TosUrl: legalDocs.TermsOfService,
SSOEnabled: sso.Enabled && sso.Issuer != "" && sso.ClientID != "", SSOEnabled: sso.Enabled && sso.Issuer != "" && sso.ClientID != "",
SSODisplayName: sso.DisplayName, SSODisplayName: sso.DisplayName,
SSOAutoRedirect: sso.AutoRedirect,
}, nil }, nil
case "explorer": case "explorer":
explorerSettings := settings.ExplorerFrontendSettings(c) explorerSettings := settings.ExplorerFrontendSettings(c)

Loading…
Cancel
Save