Skip to content

Commit 5e1923c

Browse files
committed
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. The reverse proxy or SSO provider must redirect back to Gitea for terminating the local session.
1 parent a36951a commit 5e1923c

File tree

7 files changed

+63
-3
lines changed

7 files changed

+63
-3
lines changed

custom/conf/app.example.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,13 @@ INTERNAL_TOKEN =
468468
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
469469
;REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME
470470
;;
471+
;; URL or path that Gitea should redirect users to *before* performing its
472+
;; own logout. Use this when logout is handled by a reverse proxy or SSO.
473+
;; The external logout endpoint (reverse proxy / IdP) must then redirect
474+
;; the user back to /user/logout so Gitea can terminate its local session
475+
;; after the global SSO logout completes.
476+
;REVERSE_PROXY_LOGOUT_REDIRECT = /mellon/logout?ReturnTo=/user/logout
477+
;;
471478
;; Interpret X-Forwarded-For header or the X-Real-IP header and set this as the remote IP for the request
472479
;REVERSE_PROXY_LIMIT = 1
473480
;;

modules/setting/security.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var (
2525
ReverseProxyAuthEmail string
2626
ReverseProxyAuthFullName string
2727
ReverseProxyLimit int
28+
ReverseProxyLogoutRedirect string
2829
ReverseProxyTrustedProxies []string
2930
MinPasswordLength int
3031
ImportLocalPaths bool
@@ -121,6 +122,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
121122
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
122123

123124
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
125+
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").MustString("")
124126
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
125127
if len(ReverseProxyTrustedProxies) == 0 {
126128
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}

modules/templates/helper.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ func NewFuncMap() template.FuncMap {
139139
"MermaidMaxSourceCharacters": func() int {
140140
return setting.MermaidMaxSourceCharacters
141141
},
142+
"ReverseProxyLogoutRedirect": func() string {
143+
return setting.ReverseProxyLogoutRedirect
144+
},
142145

143146
// -----------------------------------------------------------------
144147
// render

routers/web/auth/auth.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ func SignOut(ctx *context.Context) {
416416
})
417417
}
418418
HandleSignOut(ctx)
419+
if ctx.Req.Method == http.MethodGet {
420+
ctx.Redirect(setting.AppSubURL + "/")
421+
return
422+
}
419423
ctx.JSONRedirect(setting.AppSubURL + "/")
420424
}
421425

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,7 @@ func registerWebRoutes(m *web.Router) {
694694
m.Post("/recover_account", auth.ResetPasswdPost)
695695
m.Get("/forgot_password", auth.ForgotPasswd)
696696
m.Post("/forgot_password", auth.ForgotPasswdPost)
697+
m.Get("/logout", auth.SignOut)
697698
m.Post("/logout", auth.SignOut)
698699
m.Get("/stopwatches", reqSignIn, user.GetStopwatches)
699700
m.Get("/search_candidates", optExploreSignIn, user.SearchCandidates)

templates/base/head_navbar.tmpl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
</div>
5656

5757
<div class="divider"></div>
58-
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
58+
<a class="item {{if not ReverseProxyLogoutRedirect}}link-action{{end}}"
59+
{{if ReverseProxyLogoutRedirect}}href="{{ReverseProxyLogoutRedirect}}"
60+
{{else}}href data-url="{{AppSubUrl}}/user/logout"{{end}}>
5961
{{svg "octicon-sign-out"}}
6062
{{ctx.Locale.Tr "sign_out"}}
6163
</a>
@@ -128,7 +130,9 @@
128130
</a>
129131
{{end}}
130132
<div class="divider"></div>
131-
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
133+
<a class="item {{if not ReverseProxyLogoutRedirect}}link-action{{end}}"
134+
{{if ReverseProxyLogoutRedirect}}href="{{ReverseProxyLogoutRedirect}}"
135+
{{else}}href data-url="{{AppSubUrl}}/user/logout"{{end}}>
132136
{{svg "octicon-sign-out"}}
133137
{{ctx.Locale.Tr "sign_out"}}
134138
</a>

tests/integration/signout_test.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ package integration
55

66
import (
77
"net/http"
8+
"strings"
89
"testing"
910

11+
"code.gitea.io/gitea/modules/setting"
12+
"code.gitea.io/gitea/modules/test"
1013
"code.gitea.io/gitea/tests"
1114
)
1215

13-
func TestSignOut(t *testing.T) {
16+
func TestSignOut_Post(t *testing.T) {
1417
defer tests.PrepareTestEnv(t)()
1518

1619
session := loginUser(t, "user2")
@@ -22,3 +25,39 @@ func TestSignOut(t *testing.T) {
2225
req = NewRequest(t, "GET", "/user2/repo2")
2326
session.MakeRequest(t, req, http.StatusNotFound)
2427
}
28+
29+
func TestSignOut_Get(t *testing.T) {
30+
defer tests.PrepareTestEnv(t)()
31+
32+
session := loginUser(t, "user2")
33+
34+
req := NewRequest(t, "GET", "/user/logout")
35+
resp := session.MakeRequest(t, req, http.StatusSeeOther)
36+
37+
location := resp.Header().Get("Location")
38+
if location != "/" {
39+
t.Fatalf("expected redirect Location to '/', got %q", location)
40+
}
41+
42+
// try to view a private repo, should fail
43+
req = NewRequest(t, "GET", "/user2/repo2")
44+
session.MakeRequest(t, req, http.StatusNotFound)
45+
}
46+
47+
func TestSignOut_ReverseProxyLogoutRedirect(t *testing.T) {
48+
defer tests.PrepareTestEnv(t)()
49+
50+
defer test.MockVariableValue(&setting.ReverseProxyLogoutRedirect, "/mellon/logout?ReturnTo=/user/logout")()
51+
52+
session := loginUser(t, "user2")
53+
54+
req := NewRequest(t, "GET", "/")
55+
resp := session.MakeRequest(t, req, http.StatusOK)
56+
57+
body := resp.Body.String()
58+
59+
// check that the external URL is present in the logout button
60+
if !strings.Contains(body, `href="/mellon/logout?ReturnTo=/user/logout"`) {
61+
t.Fatalf("logout button does not point to REVERSE_PROXY_LOGOUT_REDIRECT when configured")
62+
}
63+
}

0 commit comments

Comments
 (0)