File opensuse-reverse-proxy-logout-redirect.patch of Package gitea

From 2bf45a526fd5723351c127f7c410817dc287d5a1 Mon Sep 17 00:00:00 2001
From: Elisei Roca <eroca@suse.de>
Date: Tue, 9 Dec 2025 17:04:41 +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 | 31 ++++++++++++++++++++++++++++++-
 5 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 33bfe752a0..d8ee91bc89 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 153b6bc944..d53097aaef 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 2ccd1c71b5..04d1e951af 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 b721779c95..0298af9f9a 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -55,7 +55,8 @@
 					</div>
 
 					<div class="divider"></div>
-					<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
+					<form id="logout-form" method="post" action="{{AppSubUrl}}/user/logout"></form>
+					<a class="item" onclick="document.getElementById('logout-form').submit();">
 						{{svg "octicon-sign-out"}}
 						{{ctx.Locale.Tr "sign_out"}}
 					</a>
@@ -121,7 +122,8 @@
 						{{ctx.Locale.Tr "help"}}
 					</a>
 					<div class="divider"></div>
-					<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
+					<form id="logout-form" method="post" action="{{AppSubUrl}}/user/logout"></form>
+					<a class="item" onclick="document.getElementById('logout-form').submit();">
 						{{svg "octicon-sign-out"}}
 						{{ctx.Locale.Tr "sign_out"}}
 					</a>
diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go
index 7fd0b5c64a..5ff42b8af7 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"
 )
 
@@ -16,7 +18,34 @@ func TestSignOut(t *testing.T) {
 	session := loginUser(t, "user2")
 
 	req := NewRequest(t, "POST", "/user/logout")
-	session.MakeRequest(t, req, http.StatusOK)
+	resp := session.MakeRequest(t, req, http.StatusSeeOther)
+
+	expected := "/"
+	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)
+}
+
+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")
-- 
2.52.0

openSUSE Build Service is sponsored by