From d093cd419b78f8cb5101a49bb78728637c82ff85 Mon Sep 17 00:00:00 2001 From: Elisei Roca Date: Mon, 8 Dec 2025 17:19:05 +0100 Subject: [PATCH] Enable logout redirection for reverse proxy setups When authentication is handled externally by a reverse proxy or SSO provider, users can be redirected to an external logout URL or relative path defined on the reverse proxy. --- custom/conf/app.example.ini | 5 +++++ modules/setting/security.go | 2 ++ routers/web/auth/auth.go | 6 +++++- templates/base/head_navbar.tmpl | 6 ++++-- tests/integration/signout_test.go | 23 +++++++++++++++++++++++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2ade8455909a9..d733e8a67c71b 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -463,6 +463,11 @@ INTERNAL_TOKEN = ;; Name of cookie used to store authentication information. ;COOKIE_REMEMBER_NAME = gitea_incredible ;; +;; URL or path that Gitea should redirect users to *after* performing its own logout. +;; Use this, if needed, when authentication is handled by a reverse proxy or SSO. +;; Mellon example: REVERSE_PROXY_LOGOUT_REDIRECT = /mellon/logout?ReturnTo=/ +;REVERSE_PROXY_LOGOUT_REDIRECT = +;; ;; Reverse proxy authentication header name of user name, email, and full name ;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER ;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL diff --git a/modules/setting/security.go b/modules/setting/security.go index 153b6bc944ff5..d53097aaef79f 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -25,6 +25,7 @@ var ( ReverseProxyAuthEmail string ReverseProxyAuthFullName string ReverseProxyLimit int + ReverseProxyLogoutRedirect string ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool @@ -121,6 +122,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME") ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1) + ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").MustString("") ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",") if len(ReverseProxyTrustedProxies) == 0 { ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"} diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 2ccd1c71b5ce3..04d1e951af496 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -416,7 +416,11 @@ func SignOut(ctx *context.Context) { }) } HandleSignOut(ctx) - ctx.JSONRedirect(setting.AppSubURL + "/") + if setting.ReverseProxyLogoutRedirect != "" { + ctx.Redirect(setting.ReverseProxyLogoutRedirect) + return + } + ctx.Redirect(setting.AppSubURL + "/") } // SignUp render the register page diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 8cb72d6f98e69..57af27c4fd4e0 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -55,7 +55,8 @@
- +
+
{{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} @@ -128,7 +129,8 @@ {{end}}
- +
+
{{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go index 7fd0b5c64a017..a2acd4cbdf940 100644 --- a/tests/integration/signout_test.go +++ b/tests/integration/signout_test.go @@ -7,6 +7,8 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" ) @@ -22,3 +24,24 @@ func TestSignOut(t *testing.T) { req = NewRequest(t, "GET", "/user2/repo2") session.MakeRequest(t, req, http.StatusNotFound) } + +func TestSignOut_ReverseProxyLogoutRedirect(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + defer test.MockVariableValue(&setting.ReverseProxyLogoutRedirect, "/mellon/logout?ReturnTo=/")() + + session := loginUser(t, "user2") + + req := NewRequest(t, "POST", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + + expected := "/mellon/logout?ReturnTo=/" + loc := resp.Header().Get("Location") + if loc != expected { + t.Fatalf("expected redirect to %q, got %q", expected, loc) + } + + // try to view a private repo, should fail + req = NewRequest(t, "GET", "/user2/repo2") + session.MakeRequest(t, req, http.StatusNotFound) +}