Skip to content

Commit 23fd5f9

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 to fully logout.
1 parent b49dd8e commit 23fd5f9

File tree

7 files changed

+62
-3
lines changed

7 files changed

+62
-3
lines changed

custom/conf/app.example.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,11 @@ INTERNAL_TOKEN =
463463
;; Name of cookie used to store authentication information.
464464
;COOKIE_REMEMBER_NAME = gitea_incredible
465465
;;
466+
;; URL or path that Gitea should redirect users to *after* performing its own logout.
467+
;; Use this to redirect user to the external logout endpoint, if needed, when authentication is handled by a reverse proxy or SSO.
468+
;; Mellon example: REVERSE_PROXY_LOGOUT_REDIRECT = /mellon/logout?ReturnTo=/
469+
;REVERSE_PROXY_LOGOUT_REDIRECT =
470+
;;
466471
;; Reverse proxy authentication header name of user name, email, and full name
467472
;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
468473
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL

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"}

routers/web/auth/auth.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,14 @@ func SignOut(ctx *context.Context) {
416416
})
417417
}
418418
HandleSignOut(ctx)
419+
if setting.ReverseProxyLogoutRedirect != "" {
420+
ctx.Redirect(setting.ReverseProxyLogoutRedirect)
421+
return
422+
}
423+
if ctx.Req.Method == http.MethodGet {
424+
ctx.Redirect(setting.AppSubURL + "/")
425+
return
426+
}
419427
ctx.JSONRedirect(setting.AppSubURL + "/")
420428
}
421429

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)

services/context/context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ func Contexter() func(next http.Handler) http.Handler {
206206
ctx.Data["ManifestData"] = setting.ManifestData
207207
ctx.Data["AllLangs"] = translation.AllLangs()
208208

209+
ctx.Data["ReverseProxyLogoutRedirect"] = setting.ReverseProxyLogoutRedirect != ""
210+
209211
next.ServeHTTP(ctx.Resp, ctx.Req)
210212
})
211213
}

templates/base/head_navbar.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
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" data-url={{else}}" href={{end}}"{{AppSubUrl}}/user/logout">
5959
{{svg "octicon-sign-out"}}
6060
{{ctx.Locale.Tr "sign_out"}}
6161
</a>
@@ -128,7 +128,7 @@
128128
</a>
129129
{{end}}
130130
<div class="divider"></div>
131-
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
131+
<a class="item{{if not .ReverseProxyLogoutRedirect}} link-action" data-url={{else}}" href={{end}}"{{AppSubUrl}}/user/logout">
132132
{{svg "octicon-sign-out"}}
133133
{{ctx.Locale.Tr "sign_out"}}
134134
</a>

tests/integration/signout_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"net/http"
88
"testing"
99

10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/test"
1012
"code.gitea.io/gitea/tests"
1113
)
1214

13-
func TestSignOut(t *testing.T) {
15+
func TestSignOut_Post(t *testing.T) {
1416
defer tests.PrepareTestEnv(t)()
1517

1618
session := loginUser(t, "user2")
@@ -22,3 +24,42 @@ func TestSignOut(t *testing.T) {
2224
req = NewRequest(t, "GET", "/user2/repo2")
2325
session.MakeRequest(t, req, http.StatusNotFound)
2426
}
27+
28+
func TestSignOut_Get(t *testing.T) {
29+
defer tests.PrepareTestEnv(t)()
30+
31+
session := loginUser(t, "user2")
32+
33+
req := NewRequest(t, "GET", "/user/logout")
34+
resp := session.MakeRequest(t, req, http.StatusSeeOther)
35+
36+
location := resp.Header().Get("Location")
37+
if location != "/" {
38+
t.Fatalf("expected redirect Location to '/', got %q", location)
39+
}
40+
41+
// try to view a private repo, should fail
42+
req = NewRequest(t, "GET", "/user2/repo2")
43+
session.MakeRequest(t, req, http.StatusNotFound)
44+
}
45+
46+
func TestSignOut_ReverseProxyLogoutRedirect(t *testing.T) {
47+
defer tests.PrepareTestEnv(t)()
48+
49+
defer test.MockVariableValue(&setting.ReverseProxyLogoutRedirect, "/mellon/logout?ReturnTo=/")()
50+
51+
session := loginUser(t, "user2")
52+
53+
req := NewRequest(t, "GET", "/user/logout")
54+
resp := session.MakeRequest(t, req, http.StatusSeeOther)
55+
56+
expected := "/mellon/logout?ReturnTo=/"
57+
loc := resp.Header().Get("Location")
58+
if loc != expected {
59+
t.Fatalf("expected redirect to %q, got %q", expected, loc)
60+
}
61+
62+
// try to view a private repo, should fail
63+
req = NewRequest(t, "GET", "/user2/repo2")
64+
session.MakeRequest(t, req, http.StatusNotFound)
65+
}

0 commit comments

Comments
 (0)