From ddaa97595d05a3be3efe8bdc6c1874531a997b85 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 20 Feb 2024 12:44:24 +0100 Subject: [PATCH] providers/proxy: rework redirect mechanism Signed-off-by: Jens Langhammer --- go.mod | 2 +- go.sum | 4 +- internal/outpost/proxyv2/application/oauth.go | 17 ++--- .../proxyv2/application/oauth_redirect.go | 67 +++++++++++++++++++ internal/outpost/proxyv2/application/test.go | 2 +- internal/outpost/proxyv2/application/utils.go | 17 ++--- internal/outpost/proxyv2/hs256/hs256.go | 4 +- 7 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 internal/outpost/proxyv2/application/oauth_redirect.go diff --git a/go.mod b/go.mod index e097edbca70..d46a72fbd3b 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-openapi/runtime v0.27.1 github.com/go-openapi/strfmt v0.22.0 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 diff --git a/go.sum b/go.sum index f5ce7097303..b219d75ca83 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/go-openapi/swag v0.22.5 h1:fVS63IE3M0lsuWRzuom3RLwUMVI2peDH01s6M70ugy github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= github.com/go-openapi/validate v0.22.4 h1:5v3jmMyIPKTR8Lv9syBAIRxG6lY0RqeBPB1LKEijzk8= github.com/go-openapi/validate v0.22.4/go.mod h1:qm6O8ZIcPVdSY5219468Jv7kBdGvkiZLPOmqnqTUZ2A= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/internal/outpost/proxyv2/application/oauth.go b/internal/outpost/proxyv2/application/oauth.go index 9f9c18d8f27..695249e9ca8 100644 --- a/internal/outpost/proxyv2/application/oauth.go +++ b/internal/outpost/proxyv2/application/oauth.go @@ -1,13 +1,11 @@ package application import ( - "encoding/base64" "net/http" "net/url" "strings" "time" - "github.com/gorilla/securecookie" "goauthentik.io/api/v3" "goauthentik.io/internal/outpost/proxyv2/constants" ) @@ -49,7 +47,6 @@ 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, a.SessionName()) // Check if we already have a state in the session, // and if we do we don't do anything here @@ -65,17 +62,17 @@ func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request) { http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(currentState), http.StatusFound) return } - rd, ok := a.checkRedirectParam(r) - if ok { - s.Values[constants.SessionRedirect] = rd - a.log.WithField("rd", rd).Trace("Setting redirect") + state, err := a.createState(r) + if err != nil { + a.log.WithError(err).Warning("failed to create state") + return } - s.Values[constants.SessionOAuthState] = newState - err := s.Save(r, rw) + s.Values[constants.SessionOAuthState] = state + err = s.Save(r, rw) if err != nil { a.log.WithError(err).Warning("failed to save session") } - http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound) + http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound) } func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) { diff --git a/internal/outpost/proxyv2/application/oauth_redirect.go b/internal/outpost/proxyv2/application/oauth_redirect.go new file mode 100644 index 00000000000..323c74a54eb --- /dev/null +++ b/internal/outpost/proxyv2/application/oauth_redirect.go @@ -0,0 +1,67 @@ +package application + +import ( + "encoding/base64" + "fmt" + "net/http" + + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/securecookie" + "goauthentik.io/internal/outpost/proxyv2/constants" +) + +type OAuthState struct { + State string `json:"state"` + Redirect string `json:"redirect"` +} + +func (oas *OAuthState) GetExpirationTime() (*jwt.NumericDate, error) { return nil, nil } +func (oas *OAuthState) GetIssuedAt() (*jwt.NumericDate, error) { return nil, nil } +func (oas *OAuthState) GetNotBefore() (*jwt.NumericDate, error) { return nil, nil } +func (oas *OAuthState) GetIssuer() (string, error) { return "goauthentik.io/outpost", nil } +func (oas *OAuthState) GetSubject() (string, error) { return oas.State, nil } +func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error) { return nil, nil } + +func (a *Application) createState(r *http.Request) (string, error) { + st := &OAuthState{ + State: base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), + } + rd, ok := a.checkRedirectParam(r) + if ok { + a.log.WithField("rd", rd).Trace("Setting redirect") + st.Redirect = rd + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, st) + tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret())) + if err != nil { + return "", err + } + return tokenString, nil +} + +func (a *Application) stateFromRequest(r *http.Request) *OAuthState { + s, err := a.sessions.Get(r, a.SessionName()) + if err != nil { + a.log.WithError(err).Trace("failed to get session") + return nil + } + stateJwt, ok := s.Values[constants.SessionOAuthState] + if !ok { + return nil + } + token, err := jwt.Parse(stateJwt.(string), func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return a.proxyConfig.CookieSecret, nil + }) + if err != nil { + a.log.WithError(err).Warning("failed to parse state jwt") + return nil + } + if claims, ok := token.Claims.(*OAuthState); ok { + return claims + } + return nil +} diff --git a/internal/outpost/proxyv2/application/test.go b/internal/outpost/proxyv2/application/test.go index 827bf797035..9cd2c89909d 100644 --- a/internal/outpost/proxyv2/application/test.go +++ b/internal/outpost/proxyv2/application/test.go @@ -45,11 +45,11 @@ func newTestApplication() *Application { Name: ak.TestSecret(), ClientId: api.PtrString(ak.TestSecret()), ClientSecret: api.PtrString(ak.TestSecret()), + CookieDomain: api.PtrString(""), CookieSecret: api.PtrString(ak.TestSecret()), ExternalHost: "https://ext.t.goauthentik.io", InternalHost: api.PtrString("http://backend"), InternalHostSslValidation: api.PtrBool(true), - CookieDomain: api.PtrString(""), Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(), SkipPathRegex: api.PtrString("/skip.*"), BasicAuthEnabled: api.PtrBool(true), diff --git a/internal/outpost/proxyv2/application/utils.go b/internal/outpost/proxyv2/application/utils.go index d895fa0be94..74df67e7f14 100644 --- a/internal/outpost/proxyv2/application/utils.go +++ b/internal/outpost/proxyv2/application/utils.go @@ -73,18 +73,13 @@ 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, a.SessionName()) - redirectR, ok := s.Values[constants.SessionRedirect] - if ok { - redirect = redirectR.(string) + fallbackRedirect := a.proxyConfig.ExternalHost + state := a.stateFromRequest(r) + if state.Redirect == "" { + state.Redirect = fallbackRedirect } - rd, ok := a.checkRedirectParam(r) - if ok { - redirect = rd - } - a.log.WithField("redirect", redirect).Trace("final redirect") - http.Redirect(rw, r, redirect, http.StatusFound) + a.log.WithField("redirect", state.Redirect).Trace("final redirect") + http.Redirect(rw, r, state.Redirect, http.StatusFound) } // toString Generic to string function, currently supports actual strings and integers diff --git a/internal/outpost/proxyv2/hs256/hs256.go b/internal/outpost/proxyv2/hs256/hs256.go index 3df8c73e82d..ba14d6f7718 100644 --- a/internal/outpost/proxyv2/hs256/hs256.go +++ b/internal/outpost/proxyv2/hs256/hs256.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "strings" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" ) type KeySet struct { @@ -22,7 +22,7 @@ func NewKeySet(secret string) *KeySet { func (ks *KeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { parts := strings.Split(jwt, ".") - err := ks.m.Verify(strings.Join(parts[0:2], "."), parts[2], []byte(ks.secret)) + err := ks.m.Verify(strings.Join(parts[0:2], "."), []byte(parts[2]), []byte(ks.secret)) if err != nil { return nil, err }