From 7d4ce41e1225e912ab73193f68d5a1e0bda02fdc Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 2 Feb 2023 21:18:59 +0100 Subject: [PATCH] providers/proxy: outpost wide logout implementation (#4605) * initial outpost wide logout implementation Signed-off-by: Jens Langhammer * handle deserialize error Signed-off-by: Jens Langhammer * update docs Signed-off-by: Jens Langhammer * fix file cleanup, add tests Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .vscode/settings.json | 3 +- .../proxyv2/application/application.go | 23 ++--- .../application/mode_forward_caddy_test.go | 3 + .../application/mode_forward_envoy_test.go | 3 + .../application/mode_forward_nginx_test.go | 3 + .../application/mode_forward_traefik_test.go | 3 + .../proxyv2/application/mode_proxy_test.go | 5 + .../outpost/proxyv2/application/session.go | 92 ++++++++++++++++++- .../proxyv2/application/session_test.go | 77 ++++++++++++++++ internal/outpost/proxyv2/application/test.go | 4 - internal/outpost/proxyv2/application/utils.go | 8 +- website/docs/providers/proxy/index.md | 2 + website/docs/releases/2023/v2023.2.md | 34 +++++++ 13 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 internal/outpost/proxyv2/application/session_test.go create mode 100644 website/docs/releases/2023/v2023.2.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ff30c275..ba5b7df63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,5 +46,6 @@ "url": "https://github.com/goauthentik/authentik/issues/", "ignoreCase": false } - ] + ], + "go.testFlags": ["-count=1"] } diff --git a/internal/outpost/proxyv2/application/application.go b/internal/outpost/proxyv2/application/application.go index 7b20274aa..47ac2a5e3 100644 --- a/internal/outpost/proxyv2/application/application.go +++ b/internal/outpost/proxyv2/application/application.go @@ -237,21 +237,22 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) { redirect := a.endpoint.EndSessionEndpoint s, err := a.sessions.Get(r, constants.SessionName) if err != nil { - http.Redirect(rw, r, redirect, http.StatusFound) + a.redirectToStart(rw, r) return } - if c, exists := s.Values[constants.SessionClaims]; c == nil || !exists { - cc := c.(Claims) - uv := url.Values{ - "id_token_hint": []string{cc.RawToken}, - } - redirect += "?" + uv.Encode() + c, exists := s.Values[constants.SessionClaims] + if c == nil && !exists { + a.redirectToStart(rw, r) + return } - s.Options.MaxAge = -1 - err = s.Save(r, rw) + cc := c.(Claims) + uv := url.Values{ + "id_token_hint": []string{cc.RawToken}, + } + redirect += "?" + uv.Encode() + err = a.Logout(cc.Sub) if err != nil { - http.Redirect(rw, r, redirect, http.StatusFound) - return + a.log.WithError(err).Warning("failed to logout of other sessions") } http.Redirect(rw, r, redirect, http.StatusFound) } diff --git a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go index f33c025f4..099018c8d 100644 --- a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go @@ -7,6 +7,7 @@ import ( "net/url" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/proxyv2/constants" @@ -69,6 +70,8 @@ func TestForwardHandleCaddy_Single_Claims(t *testing.T) { a.forwardHandleCaddy(rr, req) s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ diff --git a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go index dc56d0d0a..cfca8a991 100644 --- a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go @@ -7,6 +7,7 @@ import ( "net/url" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/proxyv2/constants" @@ -51,6 +52,8 @@ func TestForwardHandleEnvoy_Single_Claims(t *testing.T) { a.forwardHandleEnvoy(rr, req) s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ diff --git a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go index 434a18ee0..74c9f58b3 100644 --- a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/proxyv2/constants" @@ -68,6 +69,8 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) { a.forwardHandleNginx(rr, req) s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ diff --git a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go index e328bab1b..3820114b7 100644 --- a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go @@ -7,6 +7,7 @@ import ( "net/url" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/proxyv2/constants" @@ -69,6 +70,8 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) { a.forwardHandleTraefik(rr, req) s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ diff --git a/internal/outpost/proxyv2/application/mode_proxy_test.go b/internal/outpost/proxyv2/application/mode_proxy_test.go index 4ea037da2..5f4d54427 100644 --- a/internal/outpost/proxyv2/application/mode_proxy_test.go +++ b/internal/outpost/proxyv2/application/mode_proxy_test.go @@ -6,6 +6,7 @@ import ( "net/url" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "goauthentik.io/internal/outpost/proxyv2/constants" ) @@ -70,6 +71,8 @@ func TestProxy_ModifyRequest_Claims(t *testing.T) { rr := httptest.NewRecorder() s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ @@ -98,6 +101,8 @@ func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) { rr := httptest.NewRecorder() s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ Sub: "foo", Proxy: &ProxyClaims{ diff --git a/internal/outpost/proxyv2/application/session.go b/internal/outpost/proxyv2/application/session.go index a53565164..24a02e6f3 100644 --- a/internal/outpost/proxyv2/application/session.go +++ b/internal/outpost/proxyv2/application/session.go @@ -5,14 +5,21 @@ import ( "math" "net/url" "os" + "path" "strconv" + "strings" + "github.com/garyburd/redigo/redis" + "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "goauthentik.io/api/v3" "goauthentik.io/internal/config" + "goauthentik.io/internal/outpost/proxyv2/constants" "gopkg.in/boj/redistore.v1" ) +const RedisKeyPrefix = "authentik_proxy_session_" + func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store { var store sessions.Store if config.Get().Redis.Host != "" { @@ -21,7 +28,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) panic(err) } rs.SetMaxLength(math.MaxInt) - rs.SetKeyPrefix("authentik_proxy_session_") + rs.SetKeyPrefix(RedisKeyPrefix) if p.TokenValidity.IsSet() { t := p.TokenValidity.Get() // Add one to the validity to ensure we don't have a session with indefinite length @@ -55,3 +62,86 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) } return store } + +func (a *Application) Logout(sub string) error { + if fs, ok := a.sessions.(*sessions.FilesystemStore); ok { + files, err := os.ReadDir(os.TempDir()) + if err != nil { + return err + } + for _, file := range files { + s := sessions.Session{} + if !strings.HasPrefix(file.Name(), "session_") { + continue + } + fullPath := path.Join(os.TempDir(), file.Name()) + data, err := os.ReadFile(fullPath) + if err != nil { + a.log.WithError(err).Warning("failed to read file") + continue + } + err = securecookie.DecodeMulti( + constants.SessionName, string(data), + &s.Values, fs.Codecs..., + ) + if err != nil { + a.log.WithError(err).Trace("failed to decode session") + continue + } + claims := s.Values[constants.SessionClaims].(Claims) + if claims.Sub == sub { + a.log.WithField("path", fullPath).Trace("deleting session") + err := os.Remove(fullPath) + if err != nil { + a.log.WithError(err).Warning("failed to delete session") + continue + } + } + } + } + if rs, ok := a.sessions.(*redistore.RediStore); ok { + pool := rs.Pool.Get() + defer pool.Close() + rep, err := pool.Do("KEYS", fmt.Sprintf("%s*", RedisKeyPrefix)) + if err != nil { + return err + } + keys, err := redis.Strings(rep, err) + if err != nil { + return err + } + ser := redistore.GobSerializer{} + for _, key := range keys { + v, err := pool.Do("GET", key) + if err != nil { + a.log.WithError(err).Warning("failed to get value") + continue + } + b, err := redis.Bytes(v, err) + if err != nil { + a.log.WithError(err).Warning("failed to load value") + continue + } + s := sessions.Session{} + err = ser.Deserialize(b, &s) + if err != nil { + a.log.WithError(err).Warning("failed to deserialize") + continue + } + c := s.Values[constants.SessionClaims] + if c == nil { + continue + } + claims := c.(Claims) + if claims.Sub == sub { + a.log.WithField("key", key).Trace("deleting session") + _, err := pool.Do("DEL", key) + if err != nil { + a.log.WithError(err).Warning("failed to delete key") + continue + } + } + } + } + return nil +} diff --git a/internal/outpost/proxyv2/application/session_test.go b/internal/outpost/proxyv2/application/session_test.go new file mode 100644 index 000000000..5180e89a8 --- /dev/null +++ b/internal/outpost/proxyv2/application/session_test.go @@ -0,0 +1,77 @@ +package application + +import ( + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "goauthentik.io/internal/outpost/proxyv2/constants" +) + +func TestLogout(t *testing.T) { + a := newTestApplication() + _ = a.configureProxy() + req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/foo", nil) + rr := httptest.NewRecorder() + + // Login once + s, _ := a.sessions.Get(req, constants.SessionName) + s.ID = uuid.New().String() + s.Options.MaxAge = 86400 + s.Values[constants.SessionClaims] = Claims{ + Sub: "foo", + } + err := a.sessions.Save(req, rr, s) + if err != nil { + panic(err) + } + + a.mux.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadGateway, rr.Code) + + // Login twice + s2, _ := a.sessions.Get(req, constants.SessionName) + s2.ID = uuid.New().String() + s2.Options.MaxAge = 86400 + s2.Values[constants.SessionClaims] = Claims{ + Sub: "foo", + } + err = a.sessions.Save(req, rr, s2) + if err != nil { + panic(err) + } + + a.mux.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadGateway, rr.Code) + + // Logout + req, _ = http.NewRequest("GET", "https://ext.t.goauthentik.io/outpost.goauthentik.io/sign_out", nil) + s3, _ := a.sessions.Get(req, constants.SessionName) + s3.ID = uuid.New().String() + s3.Options.MaxAge = 86400 + s3.Values[constants.SessionClaims] = Claims{ + Sub: "foo", + } + err = a.sessions.Save(req, rr, s3) + if err != nil { + panic(err) + } + + rr = httptest.NewRecorder() + a.handleSignOut(rr, req) + assert.Equal(t, http.StatusFound, rr.Code) + + s1Name := filepath.Join(os.TempDir(), "session_"+s.ID) + _, err = os.Stat(s1Name) + assert.True(t, errors.Is(err, os.ErrNotExist)) + s2Name := filepath.Join(os.TempDir(), "session_"+s2.ID) + _, err = os.Stat(s2Name) + assert.True(t, errors.Is(err, os.ErrNotExist)) +} diff --git a/internal/outpost/proxyv2/application/test.go b/internal/outpost/proxyv2/application/test.go index e41eb8078..300715e23 100644 --- a/internal/outpost/proxyv2/application/test.go +++ b/internal/outpost/proxyv2/application/test.go @@ -3,7 +3,6 @@ package application import ( "net/http" - "github.com/quasoft/memstore" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/ak" ) @@ -41,8 +40,5 @@ func newTestApplication() *Application { ak.MockConfig(), ), ) - a.sessions = memstore.NewMemStore( - []byte(ak.TestSecret()), - ) return a } diff --git a/internal/outpost/proxyv2/application/utils.go b/internal/outpost/proxyv2/application/utils.go index 80f1ccd9f..41e35b831 100644 --- a/internal/outpost/proxyv2/application/utils.go +++ b/internal/outpost/proxyv2/application/utils.go @@ -74,15 +74,15 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { func (a *Application) redirect(rw http.ResponseWriter, r *http.Request) { redirect := a.proxyConfig.ExternalHost - rd, ok := a.checkRedirectParam(r) - if ok { - redirect = rd - } s, _ := a.sessions.Get(r, constants.SessionName) redirectR, ok := s.Values[constants.SessionRedirect] if ok { redirect = redirectR.(string) } + rd, ok := a.checkRedirectParam(r) + if ok { + redirect = rd + } a.log.WithField("redirect", redirect).Trace("final redirect") http.Redirect(rw, r, redirect, http.StatusFound) } diff --git a/website/docs/providers/proxy/index.md b/website/docs/providers/proxy/index.md index 023852843..6e3a6c5c5 100644 --- a/website/docs/providers/proxy/index.md +++ b/website/docs/providers/proxy/index.md @@ -73,6 +73,8 @@ When using domain-level mode, navigate to `auth.domain.tld/outpost.goauthentik.i To log out, navigate to `/outpost.goauthentik.io/sign_out`. +Starting with authentik 2023.2, when logging out of a provider, all the users sessions within the respective outpost are invalidated. + ## Allowing unauthenticated requests To allow un-authenticated requests to certain paths/URLs, you can use the _Unauthenticated URLs_ / _Unauthenticated Paths_ field. diff --git a/website/docs/releases/2023/v2023.2.md b/website/docs/releases/2023/v2023.2.md new file mode 100644 index 000000000..fcb5a77fe --- /dev/null +++ b/website/docs/releases/2023/v2023.2.md @@ -0,0 +1,34 @@ +--- +title: Release 2023.2 +slug: "/releases/2023.2" +--- + +## New features + +- Proxy provider logout improvements + + In previous versions, logging out of a single proxied application would only invalidate that application's session. Starting with this release, when logging out of a proxied application (via the _/outpost.goauthentik.io/sign_out_ URL), all the users session within the outpost are terminated. Sessions in other outposts and with other protocols are unaffected. + +## Upgrading + +This release does not introduce any new requirements. + +### docker-compose + +Download the docker-compose file for 2023.2 from [here](https://goauthentik.io/version/2023.2/docker-compose.yml). Afterwards, simply run `docker-compose up -d`. + +### Kubernetes + +Update your values to use the new images: + +```yaml +image: + repository: ghcr.io/goauthentik/server + tag: 2023.2.0 +``` + +## Minor changes/fixes + +## API Changes + +_Insert output of `make gen-diff` here_