diff --git a/go.mod b/go.mod index 128904a9b..4ee2e3332 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 goauthentik.io/api/v3 v3.2023012.5 + golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b @@ -71,7 +72,7 @@ require ( go.opentelemetry.io/otel/trace v1.11.1 // indirect golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 8f49100d5..5cceb0664 100644 --- a/go.sum +++ b/go.sum @@ -405,6 +405,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab h1:628ME69lBm9C6JY2wXhAph/yjN3jezx1z7BIDLUwxjo= +golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -528,6 +530,8 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -587,6 +591,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/outpost/proxyv2/application/application.go b/internal/outpost/proxyv2/application/application.go index 47ac2a5e3..f00720631 100644 --- a/internal/outpost/proxyv2/application/application.go +++ b/internal/outpost/proxyv2/application/application.go @@ -2,6 +2,7 @@ package application import ( "context" + "crypto/sha256" "crypto/tls" "encoding/gob" "fmt" @@ -40,6 +41,7 @@ type Application struct { oauthConfig oauth2.Config tokenVerifier *oidc.IDTokenVerifier outpostName string + sessionName string sessions sessions.Store proxyConfig api.ProxyOutpostConfig @@ -48,12 +50,19 @@ type Application struct { log *log.Entry mux *mux.Router ak *ak.APIController + srv Server errorTemplates *template.Template authHeaderCache *ttlcache.Cache[string, Claims] } -func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore, ak *ak.APIController) (*Application, error) { +type Server interface { + API() *ak.APIController + Apps() []*Application + CryptoStore() *ak.CryptoStore +} + +func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*Application, error) { gob.Register(Claims{}) muxLogger := log.WithField("logger", "authentik.outpost.proxyv2.application").WithField("name", p.Name) @@ -77,13 +86,13 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore }.Encode() managed := false - if m := ak.Outpost.Managed.Get(); m != nil { + if m := server.API().Outpost.Managed.Get(); m != nil { managed = *m == "goauthentik.io/outposts/embedded" } // Configure an OpenID Connect aware OAuth2 client. endpoint := GetOIDCEndpoint( p, - ak.Outpost.Config["authentik_host"].(string), + server.API().Outpost.Config["authentik_host"].(string), managed, ) @@ -100,10 +109,16 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore Scopes: p.ScopesToRequest, } mux := mux.NewRouter() + + h := sha256.New() + bs := string(h.Sum([]byte(*p.ClientId))) + sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8]) + a := &Application{ Host: externalHost.Host, log: muxLogger, - outpostName: ak.Outpost.Name, + outpostName: server.API().Outpost.Name, + sessionName: sessionName, endpoint: endpoint, oauthConfig: oauth2Config, tokenVerifier: verifier, @@ -111,8 +126,9 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore httpClient: c, mux: mux, errorTemplates: templates.GetTemplates(), - ak: ak, + ak: server.API(), authHeaderCache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, Claims]()), + srv: server, } go a.authHeaderCache.Start() a.sessions = a.getStore(p, externalHost) @@ -153,7 +169,9 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore }).Observe(float64(after)) }) }) - mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) + if server.API().GlobalConfig.ErrorReporting.Enabled { + mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) + } mux.Use(func(inner http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.EqualFold(r.URL.Query().Get(CallbackSignature), "true") { @@ -184,11 +202,11 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore } if kp := p.Certificate.Get(); kp != nil { - err := cs.AddKeypair(*kp) + err := server.CryptoStore().AddKeypair(*kp) if err != nil { return nil, fmt.Errorf("failed to initially fetch certificate: %w", err) } - a.Cert = cs.Get(*kp) + a.Cert = server.CryptoStore().Get(*kp) } if *p.SkipPathRegex != "" { @@ -235,7 +253,7 @@ func (a *Application) Stop() { func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) { redirect := a.endpoint.EndSessionEndpoint - s, err := a.sessions.Get(r, constants.SessionName) + s, err := a.sessions.Get(r, a.SessionName()) if err != nil { a.redirectToStart(rw, r) return diff --git a/internal/outpost/proxyv2/application/auth.go b/internal/outpost/proxyv2/application/auth.go index a6ab22c75..a17ee4536 100644 --- a/internal/outpost/proxyv2/application/auth.go +++ b/internal/outpost/proxyv2/application/auth.go @@ -49,7 +49,7 @@ func (a *Application) checkAuth(rw http.ResponseWriter, r *http.Request) (*Claim } func (a *Application) getClaimsFromSession(r *http.Request) *Claims { - s, err := a.sessions.Get(r, constants.SessionName) + s, err := a.sessions.Get(r, a.SessionName()) if err != nil { // err == user has no session/session is not valid, reject return nil @@ -77,7 +77,7 @@ func (a *Application) getClaimsFromCache(r *http.Request) *Claims { } func (a *Application) saveAndCacheClaims(rw http.ResponseWriter, r *http.Request, claims Claims) (*Claims, error) { - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) s.Values[constants.SessionClaims] = claims err := s.Save(r, rw) diff --git a/internal/outpost/proxyv2/application/error.go b/internal/outpost/proxyv2/application/error.go index 327e24a4f..8db0b48c3 100644 --- a/internal/outpost/proxyv2/application/error.go +++ b/internal/outpost/proxyv2/application/error.go @@ -20,7 +20,7 @@ func (a *Application) ErrorPage(rw http.ResponseWriter, r *http.Request, err str Message: "Error proxying to upstream server", ProxyPrefix: "/outpost.goauthentik.io", } - if claims != nil && claims.Proxy.IsSuperuser { + if claims != nil && claims.Proxy != nil && claims.Proxy.IsSuperuser { data.Message = err } else { data.Message = "Failed to connect to backend." diff --git a/internal/outpost/proxyv2/application/mode_forward.go b/internal/outpost/proxyv2/application/mode_forward.go index 757d7fbe6..47b5dd47b 100644 --- a/internal/outpost/proxyv2/application/mode_forward.go +++ b/internal/outpost/proxyv2/application/mode_forward.go @@ -64,7 +64,7 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque // to a (possibly) different domain, but we want to be redirected back // to the application // X-Forwarded-Uri is only the path, so we need to build the entire URL - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet { s.Values[constants.SessionRedirect] = fwd.String() err = s.Save(r, rw) @@ -115,7 +115,7 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request // to a (possibly) different domain, but we want to be redirected back // to the application // X-Forwarded-Uri is only the path, so we need to build the entire URL - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet { s.Values[constants.SessionRedirect] = fwd.String() err = s.Save(r, rw) @@ -151,7 +151,7 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request return } - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet { s.Values[constants.SessionRedirect] = fwd.String() err = s.Save(r, rw) @@ -190,7 +190,7 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request // to a (possibly) different domain, but we want to be redirected back // to the application // X-Forwarded-Uri is only the path, so we need to build the entire URL - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) if _, redirectSet := s.Values[constants.SessionRedirect]; !redirectSet { s.Values[constants.SessionRedirect] = fwd.String() err = s.Save(r, rw) diff --git a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go index 099018c8d..b762ce8e7 100644 --- a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go @@ -48,7 +48,7 @@ func TestForwardHandleCaddy_Single_Headers(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, "redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"}, @@ -69,7 +69,7 @@ func TestForwardHandleCaddy_Single_Claims(t *testing.T) { rr := httptest.NewRecorder() a.forwardHandleCaddy(rr, req) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -135,7 +135,7 @@ func TestForwardHandleCaddy_Domain_Header(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, "redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"}, diff --git a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go index cfca8a991..a55314247 100644 --- a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go @@ -33,7 +33,7 @@ func TestForwardHandleEnvoy_Single_Headers(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, "redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"}, @@ -51,7 +51,7 @@ func TestForwardHandleEnvoy_Single_Claims(t *testing.T) { rr := httptest.NewRecorder() a.forwardHandleEnvoy(rr, req) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -103,7 +103,7 @@ func TestForwardHandleEnvoy_Domain_Header(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, diff --git a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go index 74c9f58b3..13eaa6231 100644 --- a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go @@ -42,7 +42,7 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, rr.Code) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) } @@ -56,7 +56,7 @@ func TestForwardHandleNginx_Single_URI(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, rr.Code) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "/app", s.Values[constants.SessionRedirect]) } @@ -68,7 +68,7 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) { rr := httptest.NewRecorder() a.forwardHandleNginx(rr, req) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -132,6 +132,6 @@ func TestForwardHandleNginx_Domain_Header(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, rr.Code) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) } diff --git a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go index 3820114b7..6b3b20c82 100644 --- a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go +++ b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go @@ -48,7 +48,7 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, "redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"}, @@ -69,7 +69,7 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) { rr := httptest.NewRecorder() a.forwardHandleTraefik(rr, req) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -135,7 +135,7 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) { assert.Equal(t, http.StatusFound, rr.Code) loc, _ := rr.Result().Location() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) shouldUrl := url.Values{ "client_id": []string{*a.proxyConfig.ClientId}, "redirect_uri": []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"}, diff --git a/internal/outpost/proxyv2/application/mode_proxy_test.go b/internal/outpost/proxyv2/application/mode_proxy_test.go index 5f4d54427..a33f7cb96 100644 --- a/internal/outpost/proxyv2/application/mode_proxy_test.go +++ b/internal/outpost/proxyv2/application/mode_proxy_test.go @@ -70,7 +70,7 @@ func TestProxy_ModifyRequest_Claims(t *testing.T) { } rr := httptest.NewRecorder() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -100,7 +100,7 @@ func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) { } rr := httptest.NewRecorder() - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ diff --git a/internal/outpost/proxyv2/application/oauth.go b/internal/outpost/proxyv2/application/oauth.go index 19ab591cc..41f3f311b 100644 --- a/internal/outpost/proxyv2/application/oauth.go +++ b/internal/outpost/proxyv2/application/oauth.go @@ -45,7 +45,7 @@ func (a *Application) checkRedirectParam(r *http.Request) (string, bool) { func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request) { newState := base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) // Check if we already have a state in the session, // and if we do we don't do anything here currentState, ok := s.Values[constants.SessionOAuthState].(string) @@ -74,7 +74,7 @@ func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request) { } func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) { - s, err := a.sessions.Get(r, constants.SessionName) + s, err := a.sessions.Get(r, a.SessionName()) if err != nil { a.log.WithError(err).Trace("failed to get session") } diff --git a/internal/outpost/proxyv2/application/session.go b/internal/outpost/proxyv2/application/session.go index 8a3a7f944..5861df8cb 100644 --- a/internal/outpost/proxyv2/application/session.go +++ b/internal/outpost/proxyv2/application/session.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/sessions" "goauthentik.io/api/v3" "goauthentik.io/internal/config" + "goauthentik.io/internal/outpost/proxyv2/codecs" "goauthentik.io/internal/outpost/proxyv2/constants" "gopkg.in/boj/redistore.v1" ) @@ -21,50 +22,55 @@ import ( 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 != "" { - rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.DB), []byte(*p.CookieSecret)) + maxAge := 0 + if p.AccessTokenValidity.IsSet() { + t := p.AccessTokenValidity.Get() + // Add one to the validity to ensure we don't have a session with indefinite length + maxAge = int(*t) + 1 + } + if config.Get().Redis.Host == "foo" { + rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.DB)) if err != nil { panic(err) } + rs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret)) rs.SetMaxLength(math.MaxInt) rs.SetKeyPrefix(RedisKeyPrefix) - if p.AccessTokenValidity.IsSet() { - t := p.AccessTokenValidity.Get() - // Add one to the validity to ensure we don't have a session with indefinite length - rs.SetMaxAge(int(*t) + 1) - } else { - rs.SetMaxAge(0) - } + rs.Options.Domain = *p.CookieDomain a.log.Trace("using redis session backend") - store = rs - } else { - dir := os.TempDir() - cs := sessions.NewFilesystemStore(dir, []byte(*p.CookieSecret)) - // https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7 - // set the maxLength of the cookies stored on the disk to a larger number to prevent issues with: - // securecookie: the value is too long - // when using OpenID Connect , since this can contain a large amount of extra information in the id_token - - // Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk - cs.MaxLength(math.MaxInt) - if p.AccessTokenValidity.IsSet() { - t := p.AccessTokenValidity.Get() - // Add one to the validity to ensure we don't have a session with indefinite length - cs.MaxAge(int(*t) + 1) - } else { - cs.MaxAge(0) - } - cs.Options.Domain = *p.CookieDomain - a.log.WithField("dir", dir).Trace("using filesystem session backend") - store = cs + return rs } - return store + dir := os.TempDir() + cs := sessions.NewFilesystemStore(dir) + cs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret)) + // https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7 + // set the maxLength of the cookies stored on the disk to a larger number to prevent issues with: + // securecookie: the value is too long + // when using OpenID Connect , since this can contain a large amount of extra information in the id_token + + // Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk + cs.MaxLength(math.MaxInt) + cs.Options.Domain = *p.CookieDomain + a.log.WithField("dir", dir).Trace("using filesystem session backend") + return cs +} + +func (a *Application) SessionName() string { + return a.sessionName +} + +func (a *Application) getAllCodecs() []securecookie.Codec { + apps := a.srv.Apps() + cs := []securecookie.Codec{} + for _, app := range apps { + cs = append(cs, codecs.CodecsFromPairs(0, []byte(*app.proxyConfig.CookieSecret))...) + } + return cs } func (a *Application) Logout(sub string) error { - if fs, ok := a.sessions.(*sessions.FilesystemStore); ok { + if _, ok := a.sessions.(*sessions.FilesystemStore); ok { files, err := os.ReadDir(os.TempDir()) if err != nil { return err @@ -81,8 +87,8 @@ func (a *Application) Logout(sub string) error { continue } err = securecookie.DecodeMulti( - constants.SessionName, string(data), - &s.Values, fs.Codecs..., + a.SessionName(), string(data), + &s.Values, a.getAllCodecs()..., ) if err != nil { a.log.WithError(err).Trace("failed to decode session") diff --git a/internal/outpost/proxyv2/application/session_test.go b/internal/outpost/proxyv2/application/session_test.go index 5180e89a8..66ea11dd4 100644 --- a/internal/outpost/proxyv2/application/session_test.go +++ b/internal/outpost/proxyv2/application/session_test.go @@ -20,7 +20,7 @@ func TestLogout(t *testing.T) { rr := httptest.NewRecorder() // Login once - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) s.ID = uuid.New().String() s.Options.MaxAge = 86400 s.Values[constants.SessionClaims] = Claims{ @@ -36,7 +36,7 @@ func TestLogout(t *testing.T) { assert.Equal(t, http.StatusBadGateway, rr.Code) // Login twice - s2, _ := a.sessions.Get(req, constants.SessionName) + s2, _ := a.sessions.Get(req, a.SessionName()) s2.ID = uuid.New().String() s2.Options.MaxAge = 86400 s2.Values[constants.SessionClaims] = Claims{ @@ -53,7 +53,7 @@ func TestLogout(t *testing.T) { // Logout req, _ = http.NewRequest("GET", "https://ext.t.goauthentik.io/outpost.goauthentik.io/sign_out", nil) - s3, _ := a.sessions.Get(req, constants.SessionName) + s3, _ := a.sessions.Get(req, a.SessionName()) s3.ID = uuid.New().String() s3.Options.MaxAge = 86400 s3.Values[constants.SessionClaims] = Claims{ diff --git a/internal/outpost/proxyv2/application/test.go b/internal/outpost/proxyv2/application/test.go index 300715e23..b2d693ac0 100644 --- a/internal/outpost/proxyv2/application/test.go +++ b/internal/outpost/proxyv2/application/test.go @@ -7,7 +7,39 @@ import ( "goauthentik.io/internal/outpost/ak" ) +type testServer struct { + api *ak.APIController + apps []*Application +} + +func newTestServer() *testServer { + return &testServer{ + api: ak.MockAK( + api.Outpost{ + Config: map[string]interface{}{ + "authentik_host": ak.TestSecret(), + }, + }, + ak.MockConfig(), + ), + apps: make([]*Application, 0), + } +} + +func (ts *testServer) API() *ak.APIController { + return ts.api +} + +func (ts *testServer) CryptoStore() *ak.CryptoStore { + return nil +} + +func (ts *testServer) Apps() []*Application { + return ts.apps +} + func newTestApplication() *Application { + ts := newTestServer() a, _ := NewApplication( api.ProxyOutpostConfig{ Name: ak.TestSecret(), @@ -30,15 +62,8 @@ func newTestApplication() *Application { }, }, http.DefaultClient, - nil, - ak.MockAK( - api.Outpost{ - Config: map[string]interface{}{ - "authentik_host": ak.TestSecret(), - }, - }, - ak.MockConfig(), - ), + ts, ) + ts.apps = append(ts.apps, a) return a } diff --git a/internal/outpost/proxyv2/application/utils.go b/internal/outpost/proxyv2/application/utils.go index 41e35b831..d895fa0be 100644 --- a/internal/outpost/proxyv2/application/utils.go +++ b/internal/outpost/proxyv2/application/utils.go @@ -30,7 +30,7 @@ func urlJoin(originalUrl string, newPath string) string { } func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { - s, err := a.sessions.Get(r, constants.SessionName) + s, err := a.sessions.Get(r, a.SessionName()) if err != nil { a.log.WithError(err).Warning("failed to decode session") } @@ -74,7 +74,7 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { func (a *Application) redirect(rw http.ResponseWriter, r *http.Request) { redirect := a.proxyConfig.ExternalHost - s, _ := a.sessions.Get(r, constants.SessionName) + s, _ := a.sessions.Get(r, a.SessionName()) redirectR, ok := s.Values[constants.SessionRedirect] if ok { redirect = redirectR.(string) diff --git a/internal/outpost/proxyv2/application/utils_test.go b/internal/outpost/proxyv2/application/utils_test.go index 252d9af51..7fa5518c4 100644 --- a/internal/outpost/proxyv2/application/utils_test.go +++ b/internal/outpost/proxyv2/application/utils_test.go @@ -23,7 +23,7 @@ func TestRedirectToStart_Proxy(t *testing.T) { loc, _ := rr.Result().Location() assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start?rd=https%3A%2F%2Ftest.goauthentik.io%2Ffoo%2Fbar%2Fbaz", loc.String()) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) } @@ -40,7 +40,7 @@ func TestRedirectToStart_Forward(t *testing.T) { loc, _ := rr.Result().Location() assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start?rd=https%3A%2F%2Ftest.goauthentik.io%2Ffoo%2Fbar%2Fbaz", loc.String()) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) } @@ -58,7 +58,7 @@ func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) { loc, _ := rr.Result().Location() assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start?rd=https%3A%2F%2Ftest.goauthentik.io", loc.String()) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) } @@ -76,6 +76,6 @@ func TestRedirectToStart_Forward_Domain(t *testing.T) { loc, _ := rr.Result().Location() assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start?rd=https%3A%2F%2Ftest.goauthentik.io", loc.String()) - s, _ := a.sessions.Get(req, constants.SessionName) + s, _ := a.sessions.Get(req, a.SessionName()) assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) } diff --git a/internal/outpost/proxyv2/codecs/codec.go b/internal/outpost/proxyv2/codecs/codec.go new file mode 100644 index 000000000..472a25ed0 --- /dev/null +++ b/internal/outpost/proxyv2/codecs/codec.go @@ -0,0 +1,40 @@ +package codecs + +import ( + "github.com/gorilla/securecookie" + log "github.com/sirupsen/logrus" +) + +type Codec struct { + *securecookie.SecureCookie +} + +func New(maxAge int, hashKey, blockKey []byte) *Codec { + cookie := securecookie.New(hashKey, blockKey) + cookie.MaxAge(maxAge) + return &Codec{ + SecureCookie: cookie, + } +} + +func CodecsFromPairs(maxAge int, keyPairs ...[]byte) []securecookie.Codec { + codecs := make([]securecookie.Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(maxAge, keyPairs[i], blockKey) + } + return codecs +} + +func (s *Codec) Encode(name string, value interface{}) (string, error) { + log.Trace("cookie encode") + return s.SecureCookie.Encode("authentik_proxy", value) +} + +func (s *Codec) Decode(name string, value string, dst interface{}) error { + log.Trace("cookie decode") + return s.SecureCookie.Decode("authentik_proxy", value, dst) +} diff --git a/internal/outpost/proxyv2/constants/constants.go b/internal/outpost/proxyv2/constants/constants.go index 837a19602..cdb3fcffe 100644 --- a/internal/outpost/proxyv2/constants/constants.go +++ b/internal/outpost/proxyv2/constants/constants.go @@ -1,7 +1,5 @@ package constants -const SessionName = "authentik_proxy" - const SessionOAuthState = "oauth_state" const SessionClaims = "claims" diff --git a/internal/outpost/proxyv2/proxyv2.go b/internal/outpost/proxyv2/proxyv2.go index 1271c39bf..11338fcc6 100644 --- a/internal/outpost/proxyv2/proxyv2.go +++ b/internal/outpost/proxyv2/proxyv2.go @@ -50,7 +50,9 @@ func NewProxyServer(ac *ak.APIController) *ProxyServer { globalMux := rootMux.NewRoute().Subrouter() globalMux.Use(web.NewLoggingHandler(l.WithField("logger", "authentik.outpost.proxyv2.http"), nil)) - globalMux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) + if ac.GlobalConfig.ErrorReporting.Enabled { + globalMux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) + } s := &ProxyServer{ cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi), apps: make(map[string]*application.Application), diff --git a/internal/outpost/proxyv2/refresh.go b/internal/outpost/proxyv2/refresh.go index 2db85e76e..357044c46 100644 --- a/internal/outpost/proxyv2/refresh.go +++ b/internal/outpost/proxyv2/refresh.go @@ -10,6 +10,7 @@ import ( "goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/outpost/proxyv2/application" "goauthentik.io/internal/utils/web" + "golang.org/x/exp/maps" ) func (ps *ProxyServer) Refresh() error { @@ -33,7 +34,7 @@ func (ps *ProxyServer) Refresh() error { ), ), } - a, err := application.NewApplication(provider, hc, ps.cryptoStore, ps.akAPI) + a, err := application.NewApplication(provider, hc, ps) existing, ok := apps[a.Host] if ok { existing.Stop() @@ -48,3 +49,15 @@ func (ps *ProxyServer) Refresh() error { ps.log.Debug("Swapped maps") return nil } + +func (ps *ProxyServer) API() *ak.APIController { + return ps.akAPI +} + +func (ps *ProxyServer) CryptoStore() *ak.CryptoStore { + return ps.cryptoStore +} + +func (ps *ProxyServer) Apps() []*application.Application { + return maps.Values(ps.apps) +}