diff --git a/go.mod b/go.mod index 2f7301b..721a60e 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,25 @@ module github.com/StiviiK/keycloak-traefik-forward-auth -go 1.21 +go 1.24 -toolchain go1.22.2 +toolchain go1.24.2 require ( github.com/caarlos0/env v3.5.0+incompatible - github.com/coreos/go-oidc/v3 v3.12.0 + github.com/coreos/go-oidc/v3 v3.14.1 github.com/google/uuid v1.6.0 github.com/sirupsen/logrus v1.9.3 - golang.org/x/oauth2 v0.25.0 + golang.org/x/oauth2 v0.30.0 ) -require github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect - require ( - github.com/go-jose/go-jose/v4 v4.0.4 // indirect - github.com/tg123/go-htpasswd v1.2.3 - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/sys v0.29.0 // indirect + github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect + github.com/pzentenoe/go-cache v1.0.0 // indirect +) + +require ( + github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/tg123/go-htpasswd v1.2.4 + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/sys v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 98a24d3..e30aad2 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,37 @@ -github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= -github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= -github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= -github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pzentenoe/go-cache v1.0.0 h1:6jHsrh4CGKSBBmvNrEDn+EN9cJd4qOqLsHb7xWWEPBM= +github.com/pzentenoe/go-cache v1.0.0/go.mod h1:1JaNc73+p1tmcbNJwK55vtPR40h0hIoqqjlnhBZevBw= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tg123/go-htpasswd v1.2.3 h1:ALR6ZBIc2m9u70m+eAWUFt5p43ISbIvAvRFYzZPTOY8= -github.com/tg123/go-htpasswd v1.2.3/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU= +github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/forwardauth/auth.go b/pkg/forwardauth/auth.go index 13e38b5..1d1d98a 100644 --- a/pkg/forwardauth/auth.go +++ b/pkg/forwardauth/auth.go @@ -44,15 +44,22 @@ func (fw *ForwardAuth) IsAuthenticated(context context.Context, logger *logrus.E var claims Claims logger = logger.WithField("FunctionSource", "IsAuthenticated") - // Check if we have an Auth cookie - cookie, err := fw.GetAuthCookie(r) + // Check if we have a session cookie + cookie, err := fw.GetSessionCookie(r, options) if err != nil { logger.Error(err.Error()) return &claims, err } + sessionId := cookie.Value + session := fw.SessionCache.Get(sessionId) + if session == nil { + err = errors.New("session not found") + return &claims, err + } + // check if the token is valid - idToken, err := fw.OidcVefifier.Verify(context, cookie.Value) + idToken, err := fw.OidcVefifier.Verify(context, session.IDToken) switch { case err == nil: // Token is valid @@ -65,36 +72,24 @@ func (fw *ForwardAuth) IsAuthenticated(context context.Context, logger *logrus.E } return &claims, nil - - // Todo: Updating the cookies does sadly not work here case strings.Contains(err.Error(), "expired"): // Token is expired logger.Info("Received expired token, trying to refesh it.") - refreshCookie, err := fw.GetRefreshAuthCookie(r) + result, err := fw.RefreshToken(context, session.RefreshToken) if err != nil { + fw.SessionCache.Delete(sessionId) logger.Error(err.Error()) return &claims, err } - result, err := fw.RefreshToken(context, refreshCookie.Value) - if err != nil { - logger.Error(err.Error()) - return &claims, err - } - - http.SetCookie(w, fw.MakeAuthCookie(options, result)) - if len(result.RefreshToken) > 0 { // Do we have an refresh token? - http.SetCookie(w, fw.MakeRefreshAuthCookie(options, result)) - } + newSession := SessionCacheItem{IDToken: result.IDToken, RefreshToken: result.RefreshToken} + fw.SessionCache.Update(sessionId, &newSession) return result.IDTokenClaims, nil - case err != nil: // Other error + default: // Other error + fw.SessionCache.Delete(sessionId) logger.Error(err.Error()) return &claims, err - - default: - logger.Error("default case, should not happen") - return &claims, errors.New("default case") } } diff --git a/pkg/forwardauth/cookies.go b/pkg/forwardauth/cookies.go index f38647b..5ef7715 100644 --- a/pkg/forwardauth/cookies.go +++ b/pkg/forwardauth/cookies.go @@ -26,15 +26,15 @@ func getBaseCookie(options *options.Options) *http.Cookie { func (fw *ForwardAuth) MakeCSRFCookie(w http.ResponseWriter, r *http.Request, options *options.Options, state string) *http.Cookie { cookie := getBaseCookie(options) - cookie.Name = "__auth_csrf" + cookie.Name = options.CookiePrefix + "csrf" cookie.Value = fmt.Sprintf("%s|%s", fw.GetReturnUri(r), state) cookie.Expires = time.Now().Local().Add(time.Hour) return cookie } -func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request) (state string, redirect string, error error) { - csrfCookie, err := r.Cookie("__auth_csrf") +func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request, options *options.Options) (state string, redirect string, error error) { + csrfCookie, err := r.Cookie(options.CookiePrefix + "csrf") if err != nil { return "", "", errors.New("Missing csrf cookie") } @@ -58,49 +58,28 @@ func (fw *ForwardAuth) ValidateCSRFCookie(r *http.Request) (state string, redire func (fw *ForwardAuth) ClearCSRFCookie(options *options.Options) *http.Cookie { cookie := getBaseCookie(options) - cookie.Name = "__auth_csrf" + cookie.Name = options.CookiePrefix + "csrf" cookie.Expires = time.Now().Local().Add(time.Hour * -1) return cookie } -func (fw *ForwardAuth) MakeAuthCookie(options *options.Options, authResult *AuthenticatationResult) *http.Cookie { +func (fw *ForwardAuth) MakeSessionCookie(options *options.Options, sessionId string) *http.Cookie { cookie := getBaseCookie(options) - cookie.Name = "__auth" - cookie.Value = authResult.IDToken - cookie.Expires = time.Now().Local().Add(time.Hour * 24) + cookie.Name = options.CookiePrefix + "session" + cookie.Value = sessionId + cookie.Expires = time.Now().Local().Add(time.Hour * time.Duration(options.SessionLifetime)) return cookie } -func (fw *ForwardAuth) GetAuthCookie(r *http.Request) (*http.Cookie, error) { - return r.Cookie("__auth") +func (fw *ForwardAuth) GetSessionCookie(r *http.Request, options *options.Options) (*http.Cookie, error) { + return r.Cookie(options.CookiePrefix + "session") } -func (fw *ForwardAuth) ClearAuthCookie(options *options.Options) *http.Cookie { +func (fw *ForwardAuth) ClearSessionCookie(options *options.Options) *http.Cookie { cookie := getBaseCookie(options) - cookie.Name = "__auth" - cookie.Expires = time.Now().Local().Add(time.Hour * -1) - - return cookie -} - -func (fw *ForwardAuth) MakeRefreshAuthCookie(options *options.Options, authResult *AuthenticatationResult) *http.Cookie { - cookie := getBaseCookie(options) - cookie.Name = "__auth_refresh" - cookie.Value = authResult.RefreshToken - cookie.Expires = time.Now().Local().Add(time.Hour * 24) - - return cookie -} - -func (fw *ForwardAuth) GetRefreshAuthCookie(r *http.Request) (*http.Cookie, error) { - return r.Cookie("__auth_refresh") -} - -func (fw *ForwardAuth) ClearRefreshAuthCookie(options *options.Options) *http.Cookie { - cookie := getBaseCookie(options) - cookie.Name = "__auth_refresh" + cookie.Name = options.CookiePrefix + "session" cookie.Expires = time.Now().Local().Add(time.Hour * -1) return cookie diff --git a/pkg/forwardauth/forwardauth.go b/pkg/forwardauth/forwardauth.go index b10c563..3177a9e 100644 --- a/pkg/forwardauth/forwardauth.go +++ b/pkg/forwardauth/forwardauth.go @@ -20,6 +20,7 @@ type ForwardAuth struct { OidcProvider *oidc.Provider OAuth2Config oauth2.Config OidcVefifier *oidc.IDTokenVerifier + SessionCache SessionCache } // Claims represents the claims struct which we get from the identity provider @@ -68,5 +69,6 @@ func Create(ctx context.Context, options *options.Options) (*ForwardAuth, error) Scopes: scopes, }, OidcVefifier: verifier, + SessionCache: newSessionCache(options), }, nil } diff --git a/pkg/forwardauth/session.go b/pkg/forwardauth/session.go new file mode 100644 index 0000000..28f75e9 --- /dev/null +++ b/pkg/forwardauth/session.go @@ -0,0 +1,51 @@ +package forwardauth + +import ( + "time" + + "github.com/StiviiK/keycloak-traefik-forward-auth/pkg/options" + "github.com/google/uuid" + "github.com/pzentenoe/go-cache" +) + +type SessionCache struct { + internal *cache.Cache +} + +type SessionCacheItem struct { + IDToken string + RefreshToken string +} + +func newSessionCache(options *options.Options) SessionCache { + return SessionCache{ + internal: cache.New(time.Hour*time.Duration(options.SessionLifetime), time.Hour), + } +} + +func (c *SessionCache) Get(sessionId string) *SessionCacheItem { + itm, _ := c.internal.Get(sessionId) + if itm == nil { + return nil + } + return itm.(*SessionCacheItem) +} + +func (c *SessionCache) Create(session *SessionCacheItem) string { + sessionId := uuid.New().String() + c.internal.SetDefault(sessionId, session) + return sessionId +} + +func (c *SessionCache) Update(sessionId string, session *SessionCacheItem) { + _, exp, found := c.internal.GetWithExpiration(sessionId) + if found { + c.internal.Set(sessionId, session, exp.Sub(time.Now())) + } else { + c.internal.SetDefault(sessionId, session) + } +} + +func (c *SessionCache) Delete(sessionId string) { + c.internal.Delete(sessionId) +} diff --git a/pkg/httphandler/callback.go b/pkg/httphandler/callback.go index 9585ac9..78ae503 100644 --- a/pkg/httphandler/callback.go +++ b/pkg/httphandler/callback.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" + "github.com/StiviiK/keycloak-traefik-forward-auth/pkg/forwardauth" "github.com/sirupsen/logrus" ) @@ -19,7 +20,7 @@ func (root *HttpHandler) callbackHandler(w http.ResponseWriter, r *http.Request, }) // check for the csrf cookie - state, redirect, err := root.forwardAuth.ValidateCSRFCookie(r) + state, redirect, err := root.forwardAuth.ValidateCSRFCookie(r, root.options) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -41,9 +42,9 @@ func (root *HttpHandler) callbackHandler(w http.ResponseWriter, r *http.Request, // clear the csrf cookie http.SetCookie(w, root.forwardAuth.ClearCSRFCookie(root.options)) - http.SetCookie(w, root.forwardAuth.MakeAuthCookie(root.options, authResult)) - //if len(authResult.RefreshToken) > 0 { // Do we have an refresh token? - // http.SetCookie(w, root.forwardAuth.MakeRefreshAuthCookie(root.options, authResult)) - //} + newSession := forwardauth.SessionCacheItem{IDToken: authResult.IDToken, RefreshToken: authResult.RefreshToken} + sessionId := root.forwardAuth.SessionCache.Create(&newSession) + + http.SetCookie(w, root.forwardAuth.MakeSessionCookie(root.options, sessionId)) http.Redirect(w, r, redirect, http.StatusTemporaryRedirect) } diff --git a/pkg/httphandler/root.go b/pkg/httphandler/root.go index f3d4767..736b47f 100644 --- a/pkg/httphandler/root.go +++ b/pkg/httphandler/root.go @@ -38,8 +38,7 @@ func (root *HttpHandler) rootHandler(w http.ResponseWriter, r *http.Request, for logger = logger.WithField("FunctionSource", "RootHandler") logger.Warn("IsAuthenticated failed, initating login flow.") - http.SetCookie(w, root.forwardAuth.ClearAuthCookie(root.options)) - //http.SetCookie(w, root.forwardAuth.ClearRefreshAuthCookie(root.options)) + http.SetCookie(w, root.forwardAuth.ClearSessionCookie(root.options)) state := uuid.New().String() http.SetCookie(w, root.forwardAuth.MakeCSRFCookie(w, r, root.options, state)) diff --git a/pkg/options/options.go b/pkg/options/options.go index 3a1477d..39b1172 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -13,17 +13,19 @@ import ( ) type Options struct { - Issuer string `env:"ISSUER"` - ClientID string `env:"CLIENT_ID"` - ClientSecret string `env:"CLIENT_SECRET"` - AuthDomain string `env:"AUTH_DOMAIN"` - CookieDomain string `env:"COOKIE_DOMAIN"` - Port int `env:"PORT" envDefault:"4181"` - RedirectURL string `env:"REDIRECT_URL" envDefault:"/auth/resp"` - Scopes string `env:"SCOPES"` - BypassUser string `env:"BYPASS_USER"` - BypassFile string `env:"BYPASS_FILE"` - BypassPwd *htpasswd.File + Issuer string `env:"ISSUER"` + ClientID string `env:"CLIENT_ID"` + ClientSecret string `env:"CLIENT_SECRET"` + AuthDomain string `env:"AUTH_DOMAIN"` + CookieDomain string `env:"COOKIE_DOMAIN"` + CookiePrefix string `env:"COOKIE_PREFIX" envDefault:"oidca_"` + Port int `env:"PORT" envDefault:"4181"` + RedirectURL string `env:"REDIRECT_URL" envDefault:"/auth/resp"` + Scopes string `env:"SCOPES"` + BypassUser string `env:"BYPASS_USER"` + BypassFile string `env:"BYPASS_FILE"` + BypassPwd *htpasswd.File + SessionLifetime int `env:"SESSION_LIFETIME" envDefault:"24"` } // LoadOptions parses the environment vars and the options