diff --git a/authenticator_kubernetes.go b/authenticator_kubernetes.go index 2d446380..552b9ae6 100644 --- a/authenticator_kubernetes.go +++ b/authenticator_kubernetes.go @@ -57,3 +57,10 @@ func (k8sauth *kubernetesAuthenticator) AuthenticateRequest(r *http.Request) (*a return resp, found, err } + +// The Kubernetes Authenticator implements the Cacheable +// interface with the getCacheKey(). +func (k8sauth *kubernetesAuthenticator) getCacheKey(r *http.Request) (string) { + return getBearerToken(r.Header.Get("Authorization")) + +} diff --git a/go.mod b/go.mod index a3b8777c..9c7a4973 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/gorilla/sessions v1.2.0 github.com/kelseyhightower/envconfig v1.4.0 + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 diff --git a/go.sum b/go.sum index fdeaa858..3e89905e 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,8 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= diff --git a/main.go b/main.go index 9a49e3be..10a49d95 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( oidc "github.com/coreos/go-oidc" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" "github.com/tevino/abool" "github.com/yosssi/boltstore/shared" @@ -23,6 +24,7 @@ import ( // Issue: https://github.com/gorilla/sessions/issues/200 const secureCookieKeyPair = "notNeededBecauseCookieValueIsRandom" +const CacheCleanupInterval = 10 func main() { @@ -161,6 +163,10 @@ func main() { groupsClaim: c.GroupsClaim, } + // Set the bearerUserInfoCache cache to store + // the (Bearer Token, UserInfo) pairs. + bearerUserInfoCache := cache.New(time.Duration(c.CacheExpirationMinutes)*time.Minute, time.Duration(CacheCleanupInterval)*time.Minute) + // Set the server values. // The isReady atomic variable should protect it from concurrency issues. @@ -170,6 +176,7 @@ func main() { // TODO: Add support for Redis store: store, oidcStateStore: oidcStateStore, + bearerUserInfoCache: bearerUserInfoCache, afterLoginRedirectURL: c.AfterLoginURL.String(), homepageURL: c.HomepageURL.String(), afterLogoutRedirectURL: c.AfterLogoutURL.String(), @@ -186,6 +193,8 @@ func main() { userIdTransformer: c.UserIDTransformer, sessionMaxAgeSeconds: c.SessionMaxAge, strictSessionValidation: c.StrictSessionValidation, + cacheEnabled: c.CacheEnabled, + cacheExpirationMinutes: c.CacheExpirationMinutes, authHeader: c.AuthHeader, caBundle: caBundle, authenticators: []authenticator.Request{ @@ -205,6 +214,9 @@ func main() { s.sessionSameSite = http.SameSiteLaxMode } + // Print server configuration info + log.Infof("Cache enabled: %t", s.cacheEnabled) + // Setup complete, mark server ready isReady.Set() diff --git a/server.go b/server.go index 2f9525d9..c5366856 100644 --- a/server.go +++ b/server.go @@ -6,10 +6,13 @@ import ( "encoding/gob" "fmt" "net/http" + "reflect" "strings" + "time" oidc "github.com/coreos/go-oidc" "github.com/gorilla/sessions" + cache "github.com/patrickmn/go-cache" "github.com/pkg/errors" "github.com/tevino/abool" "golang.org/x/oauth2" @@ -43,6 +46,7 @@ type server struct { oauth2Config *oauth2.Config store sessions.Store oidcStateStore sessions.Store + bearerUserInfoCache *cache.Cache authenticators []authenticator.Request authorizers []Authorizer afterLoginRedirectURL string @@ -50,6 +54,10 @@ type server struct { afterLogoutRedirectURL string sessionMaxAgeSeconds int strictSessionValidation bool + + cacheEnabled bool + cacheExpirationMinutes int + authHeader string idTokenOpts jwtClaimOpts upstreamHTTPHeaderOpts httpHeaderOpts @@ -80,7 +88,35 @@ func (s *server) authenticate(w http.ResponseWriter, r *http.Request) { logger.Info("Authenticating request...") var userInfo user.Info + for i, auth := range s.authenticators { + var cacheKey string + + if s.cacheEnabled { + // If the cache is enabled, check if the current authenticator implements the Cacheable interface. + cacheable := reflect.TypeOf((*Cacheable)(nil)).Elem() + isCacheable := reflect.TypeOf(auth).Implements(cacheable) + + if isCacheable { + // Store the key that we are going to use for caching UserDetails. + // We store it before the authentication, because the authenticators may mutate the request object. + logger.Debugf("Retrieving the cache key...") + cacheableAuthenticator := reflect.ValueOf(auth).Interface().(Cacheable) + cacheKey = cacheableAuthenticator.getCacheKey(r) + } + } + + if cacheKey != "" { + // If caching is enabled, try to retrieve the UserInfo from cache. + userInfo = s.getCachedUserInfo(cacheKey, r) + + if userInfo != nil { + logger.Infof("Successfully authenticated request using the cache.") + logger.Infof("UserInfo: %+v", userInfo) + break + } + } + logger.Infof("%s starting...", strings.Title(authenticatorsMapping[i])) resp, found, err := auth.AuthenticateRequest(r) if err != nil { @@ -97,6 +133,12 @@ func (s *server) authenticate(w http.ResponseWriter, r *http.Request) { logger.Infof("Successfully authenticated request using %s", authenticatorsMapping[i]) userInfo = resp.User logger.Infof("UserInfo: %+v", userInfo) + + if cacheKey != "" { + // If cache is enabled and the current authenticator is Cacheable, store the UserInfo to cache. + logger.Infof("Caching authenticated UserInfo...") + s.bearerUserInfoCache.Set(cacheKey, userInfo, time.Duration(s.cacheExpirationMinutes)*time.Minute) + } break } } @@ -149,6 +191,21 @@ func (s *server) authenticate(w http.ResponseWriter, r *http.Request) { return } +// getCachedUserInfo returns the UserInfo if it's in the cache +// using the key: 'cacheKey' or it returns nil. +func (s *server) getCachedUserInfo(cacheKey string, r *http.Request) user.Info { + logger := loggerForRequest(r, logModuleInfo) + + cachedUserInfo, found := s.bearerUserInfoCache.Get(cacheKey) + if found { + userInfo := cachedUserInfo.(user.Info) + logger.Infof("Found Cached UserInfo: %+v", userInfo) + return userInfo + } + logger.Info("The UserInfo is not cached.") + return nil +} + // authCodeFlowAuthenticationRequest initiates an OIDC Authorization Code flow func (s *server) authCodeFlowAuthenticationRequest(w http.ResponseWriter, r *http.Request) { logger := loggerForRequest(r, logModuleInfo) diff --git a/settings.go b/settings.go index 7804da4d..f5bf27f7 100644 --- a/settings.go +++ b/settings.go @@ -60,6 +60,10 @@ type config struct { TemplatePath []string `split_words:"true"` UserTemplateContext map[string]string `ignored:"true"` + // bearerUserInfoCache configuration + CacheEnabled bool `split_words:"true" default:"false" envconfig:"CACHE_ENABLED"` + CacheExpirationMinutes int `split_words:"true" default:"5" envconfig:"CACHE_EXPIRATION_MINUTES"` + // Authorization GroupsAllowlist []string `split_words:"true" default:"*"` } diff --git a/util.go b/util.go index b3ecff98..9613f57b 100644 --- a/util.go +++ b/util.go @@ -20,6 +20,10 @@ import ( "k8s.io/apiserver/pkg/authentication/user" ) +type Cacheable interface { + getCacheKey(r *http.Request) string +} + func realpath(path string) (string, error) { path, err := filepath.Abs(path) if err != nil {