From d8b34c068f59dcc26c6ba8cbfdd03bdd3ddcb226 Mon Sep 17 00:00:00 2001 From: Bernd Konrad Date: Wed, 19 Aug 2026 21:03:55 +0200 Subject: [PATCH 1/3] feat: auto-redirect to OIDC IdP (#991) --- config/config.go | 2 ++ config/keys.go | 1 + docs/spec.json | 9 ++++++++- gotify-server.env.example | 7 +++++++ model/gotifyinfo.go | 6 ++++++ router/router.go | 13 +++++++------ router/router_test.go | 3 +-- ui/serve.go | 23 +++++++++++++---------- ui/src/config.ts | 2 ++ ui/src/user/Login.tsx | 25 ++++++++++++++++++------- 10 files changed, 65 insertions(+), 26 deletions(-) diff --git a/config/config.go b/config/config.go index 85ece6bba..746267390 100644 --- a/config/config.go +++ b/config/config.go @@ -69,6 +69,7 @@ type OIDC struct { LinkByUsername bool Scopes []string IDPName string + AutoRedirect bool } type Configuration struct { @@ -183,6 +184,7 @@ func Get() (*Configuration, []FutureLog) { add(parseBool(&c.OIDC.LinkByUsername, EnvOIDCLinkByUsername)) add(parseList(&c.OIDC.Scopes, EnvOIDCScopes)) add(parseString(&c.OIDC.IDPName, EnvOIDCIDPName)) + add(parseBool(&c.OIDC.AutoRedirect, EnvOIDCAutoRedirect)) add(parseString(&c.NoColor, EnvNoColor)) diff --git a/config/keys.go b/config/keys.go index c6bf7167f..91cb0c56a 100644 --- a/config/keys.go +++ b/config/keys.go @@ -43,5 +43,6 @@ const ( EnvLocalAuthEnabled = "GOTIFY_LOCALAUTH_ENABLED" EnvOIDCScopes = "GOTIFY_OIDC_SCOPES" EnvOIDCIDPName = "GOTIFY_OIDC_IDP_NAME" + EnvOIDCAutoRedirect = "GOTIFY_OIDC_AUTO_REDIRECT" EnvNoColor = "NOCOLOR" ) diff --git a/docs/spec.json b/docs/spec.json index b75c9e599..dfb37e488 100644 --- a/docs/spec.json +++ b/docs/spec.json @@ -2948,7 +2948,8 @@ "register", "localAuth", "oidc", - "oidcIdpName" + "oidcIdpName", + "oidcAutoRedirect" ], "properties": { "localAuth": { @@ -2963,6 +2964,12 @@ "x-go-name": "Oidc", "example": true }, + "oidcAutoRedirect": { + "description": "If the WebUI should automatically redirect to the OIDC identity\nprovider instead of showing the login page.", + "type": "boolean", + "x-go-name": "OIDCAutoRedirect", + "example": false + }, "oidcIdpName": { "description": "Name of the OIDC identity provider.", "type": "string", diff --git a/gotify-server.env.example b/gotify-server.env.example index e8e734460..bf31a7661 100644 --- a/gotify-server.env.example +++ b/gotify-server.env.example @@ -224,6 +224,13 @@ # Type: text-list # GOTIFY_OIDC_SCOPES=openid,profile,email +# Automatically redirect to the OIDC identity provider instead of showing the +# login page. Users can still reach the login form by visiting the WebUI login +# route with ?redirect=false, e.g. https://push.example.com/#/login?redirect=false +# +# Type: boolean +# GOTIFY_OIDC_AUTO_REDIRECT=false + # Enable authentication via username and password. # Type: boolean # GOTIFY_LOCALAUTH_ENABLED=true diff --git a/model/gotifyinfo.go b/model/gotifyinfo.go index 526276efd..4d5499fd3 100644 --- a/model/gotifyinfo.go +++ b/model/gotifyinfo.go @@ -29,4 +29,10 @@ type GotifyInfo struct { // required: true // example: OIDC OIDCIDPName string `json:"oidcIdpName"` + // If the WebUI should automatically redirect to the OIDC identity + // provider instead of showing the login page. + // + // required: true + // example: false + OIDCAutoRedirect bool `json:"oidcAutoRedirect"` } diff --git a/router/router.go b/router/router.go index 8a05316b5..74e770d3b 100644 --- a/router/router.go +++ b/router/router.go @@ -120,7 +120,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser) userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID) - ui.Register(g, *vInfo, conf.Registration, conf.LocalAuthEnabled, conf.OIDC.Enabled, conf.OIDC.IDPName) + ui.Register(g, *vInfo, conf.Registration, conf.LocalAuthEnabled, conf.OIDC.Enabled, conf.OIDC.IDPName, conf.OIDC.AutoRedirect) if conf.OIDC.Enabled { oidcHandler := api.NewOIDC(conf, db, userChangeNotifier) @@ -192,11 +192,12 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co // $ref: "#/definitions/GotifyInfo" g.GET("gotifyinfo", func(ctx *gin.Context) { ctx.JSON(200, &model.GotifyInfo{ - Version: vInfo.Version, - Oidc: conf.OIDC.Enabled, - Register: conf.Registration, - LocalAuth: conf.LocalAuthEnabled, - OIDCIDPName: conf.OIDC.IDPName, + Version: vInfo.Version, + Oidc: conf.OIDC.Enabled, + Register: conf.Registration, + LocalAuth: conf.LocalAuthEnabled, + OIDCIDPName: conf.OIDC.IDPName, + OIDCAutoRedirect: conf.OIDC.AutoRedirect, }) }) diff --git a/router/router_test.go b/router/router_test.go index bfc784416..25fccad4d 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -66,8 +66,7 @@ func (s *IntegrationSuite) TestVersionInfo() { func (s *IntegrationSuite) TestGotifyInfo() { req := s.newRequest("GET", "gotifyinfo", "") - - doRequestAndExpect(s.T(), req, 200, `{"version":"1.0.0", "oidc":false, "register":false, "localAuth":true, "oidcIdpName":"Company XYZ SSO"}`) + doRequestAndExpect(s.T(), req, 200, `{"version":"1.0.0", "oidc":false, "register":false, "localAuth":true, "oidcIdpName":"Company XYZ SSO", "oidcAutoRedirect":false}`) } func (s *IntegrationSuite) TestHeaderInProd() { diff --git a/ui/serve.go b/ui/serve.go index 503e55a39..427dc83bb 100644 --- a/ui/serve.go +++ b/ui/serve.go @@ -16,11 +16,12 @@ import ( var box embed.FS type uiConfig struct { - Register bool `json:"register"` - Version model.VersionInfo `json:"version"` - LocalAuth bool `json:"localAuth"` - OIDC bool `json:"oidc"` - OIDCIDPName string `json:"oidcIdpName"` + Register bool `json:"register"` + Version model.VersionInfo `json:"version"` + LocalAuth bool `json:"localAuth"` + OIDC bool `json:"oidc"` + OIDCIDPName string `json:"oidcIdpName"` + OIDCAutoRedirect bool `json:"oidcAutoRedirect"` } // Register registers the ui on the root path. @@ -31,13 +32,15 @@ func Register( localAuthEnabled bool, oidcEnabled bool, oidcIDPName string, + oidcAutoRedirect bool, ) { uiConfigBytes, err := json.Marshal(uiConfig{ - Version: version, - Register: register, - LocalAuth: localAuthEnabled, - OIDC: oidcEnabled, - OIDCIDPName: oidcIDPName, + Version: version, + Register: register, + LocalAuth: localAuthEnabled, + OIDC: oidcEnabled, + OIDCIDPName: oidcIDPName, + OIDCAutoRedirect: oidcAutoRedirect, }) if err != nil { panic(err) diff --git a/ui/src/config.ts b/ui/src/config.ts index be57bd213..b88baefdc 100644 --- a/ui/src/config.ts +++ b/ui/src/config.ts @@ -5,6 +5,7 @@ export interface IConfig { register: boolean; version: IVersion; oidc: boolean; + oidcAutoRedirect: boolean; localAuth: boolean; oidcIdpName: string; } @@ -20,6 +21,7 @@ const config: IConfig = { register: false, version: {commit: 'unknown', buildDate: 'unknown', version: 'unknown'}, oidc: false, + oidcAutoRedirect: false, localAuth: true, oidcIdpName: 'OIDC', ...window.config, diff --git a/ui/src/user/Login.tsx b/ui/src/user/Login.tsx index 8e6572a20..03de5bdb9 100644 --- a/ui/src/user/Login.tsx +++ b/ui/src/user/Login.tsx @@ -9,7 +9,7 @@ import * as config from '../config'; import RegistrationDialog from './Register'; import {useStores} from '../stores'; import {observer} from 'mobx-react-lite'; -import {useNavigate} from 'react-router'; +import {useNavigate, useSearchParams} from 'react-router'; const Login = observer(() => { const [username, setUsername] = React.useState(''); @@ -17,15 +17,30 @@ const Login = observer(() => { const [registerDialog, setRegisterDialog] = React.useState(false); const {currentUser} = useStores(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const localAuthEnabled = config.get('localAuth'); const oidcEnabled = config.get('oidc'); const oidcIdpName = config.get('oidcIdpName'); + const oidcAutoRedirect = + oidcEnabled && + config.get('oidcAutoRedirect') && + searchParams.get('redirect') !== 'false' && + !currentUser.connectionErrorMessage; + const oidcLoginUrl = + config.get('url') + + 'auth/oidc/login?name=' + + encodeURIComponent(currentUser.createClientName()); React.useEffect(() => { if (currentUser.loggedIn) { navigate('/'); + return; } - }, [currentUser.loggedIn]); + if (!currentUser.authenticating && oidcAutoRedirect) { + window.location.href = oidcLoginUrl; + } + }, [currentUser.loggedIn, currentUser.authenticating, oidcAutoRedirect]); + const registerButton = () => { if (localAuthEnabled && config.get('register')) return ( @@ -96,11 +111,7 @@ const Login = observer(() => {