diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2ade8455909a9..d1178390a8075 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -468,6 +468,13 @@ INTERNAL_TOKEN = ;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL ;REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME ;; +;; URL or path that Gitea should redirect users to *before* performing its +;; own logout. Use this when logout is handled by a reverse proxy or SSO. +;; The external logout endpoint (reverse proxy / IdP) must then redirect +;; the user back to /user/logout so Gitea can terminate its local session +;; after the global SSO logout completes. +;REVERSE_PROXY_LOGOUT_REDIRECT = /mellon/logout?ReturnTo=/user/logout +;; ;; Interpret X-Forwarded-For header or the X-Real-IP header and set this as the remote IP for the request ;REVERSE_PROXY_LIMIT = 1 ;; 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/modules/templates/helper.go b/modules/templates/helper.go index a7aa321811d55..a172389d99123 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -139,6 +139,9 @@ func NewFuncMap() template.FuncMap { "MermaidMaxSourceCharacters": func() int { return setting.MermaidMaxSourceCharacters }, + "ReverseProxyLogoutRedirect": func() string { + return setting.ReverseProxyLogoutRedirect + }, // ----------------------------------------------------------------- // render diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 2ccd1c71b5ce3..74bc6ed2a0066 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -416,6 +416,10 @@ func SignOut(ctx *context.Context) { }) } HandleSignOut(ctx) + if ctx.Req.Method == http.MethodGet { + ctx.Redirect(setting.AppSubURL + "/") + return + } ctx.JSONRedirect(setting.AppSubURL + "/") } diff --git a/routers/web/web.go b/routers/web/web.go index dd1b391c68983..d6c4d41e42b6e 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -694,6 +694,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/recover_account", auth.ResetPasswdPost) m.Get("/forgot_password", auth.ForgotPasswd) m.Post("/forgot_password", auth.ForgotPasswdPost) + m.Get("/logout", auth.SignOut) m.Post("/logout", auth.SignOut) m.Get("/stopwatches", reqSignIn, user.GetStopwatches) m.Get("/search_candidates", optExploreSignIn, user.SearchCandidates) diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 8cb72d6f98e69..7b149a40c2181 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -55,7 +55,9 @@
- + {{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} @@ -128,7 +130,9 @@ {{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..93fcf7b00b82a 100644 --- a/tests/integration/signout_test.go +++ b/tests/integration/signout_test.go @@ -5,12 +5,15 @@ package integration import ( "net/http" + "strings" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" ) -func TestSignOut(t *testing.T) { +func TestSignOut_Post(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") @@ -22,3 +25,39 @@ func TestSignOut(t *testing.T) { req = NewRequest(t, "GET", "/user2/repo2") session.MakeRequest(t, req, http.StatusNotFound) } + +func TestSignOut_Get(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + + location := resp.Header().Get("Location") + if location != "/" { + t.Fatalf("expected redirect Location to '/', got %q", location) + } + + // 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=/user/logout")() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/") + resp := session.MakeRequest(t, req, http.StatusOK) + + body := resp.Body.String() + + // check that the external URL is present in the logout button + if !strings.Contains(body, `href="/mellon/logout?ReturnTo=/user/logout"`) { + t.Fatalf("logout button does not point to REVERSE_PROXY_LOGOUT_REDIRECT when configured") + } +}