diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 2eed7bd78c..1e1f580505 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -719,6 +719,30 @@ }, "retry": { "$ref": "#/definitions/retry" + }, + "cache": { + "additionalProperties": false, + "type": "object", + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "ttl": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "title": "Cache Time to Live", + "description": "Can override the default behaviour of using the token exp time, and specify a set time to live for the token in the cache. If the token exp time is lower than the set value the token exp time will be used instead.", + "examples": [ + "5s" + ] + }, + "max_tokens": { + "type": "integer", + "default": 1000, + "title": "Maximum Cached Tokens", + "description": "Max number of tokens to cache." + } + } } }, "required": [ diff --git a/driver/configuration/provider_viper_public_test.go b/driver/configuration/provider_viper_public_test.go index 111e128216..4315b9f3dd 100644 --- a/driver/configuration/provider_viper_public_test.go +++ b/driver/configuration/provider_viper_public_test.go @@ -286,7 +286,7 @@ func TestViperProvider(t *testing.T) { }) t.Run("authenticator=oauth2_client_credentials", func(t *testing.T) { - a := authn.NewAuthenticatorOAuth2ClientCredentials(p) + a := authn.NewAuthenticatorOAuth2ClientCredentials(p, logger) assert.True(t, p.AuthenticatorIsEnabled(a.GetID())) require.NoError(t, a.Validate(nil)) diff --git a/driver/registry_memory.go b/driver/registry_memory.go index fda492c768..783beee6f0 100644 --- a/driver/registry_memory.go +++ b/driver/registry_memory.go @@ -369,7 +369,7 @@ func (r *RegistryMemory) prepareAuthn() { authn.NewAuthenticatorBearerToken(r.c), authn.NewAuthenticatorJWT(r.c, r), authn.NewAuthenticatorNoOp(r.c), - authn.NewAuthenticatorOAuth2ClientCredentials(r.c), + authn.NewAuthenticatorOAuth2ClientCredentials(r.c, r.Logger()), authn.NewAuthenticatorOAuth2Introspection(r.c, r.Logger()), authn.NewAuthenticatorUnauthorized(r.c), } diff --git a/internal/httpclient/models/health_not_ready_status.go b/internal/httpclient/models/health_not_ready_status.go index 64626783ed..4e697f273c 100644 --- a/internal/httpclient/models/health_not_ready_status.go +++ b/internal/httpclient/models/health_not_ready_status.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag" ) -// HealthNotReadyStatus health not ready status +// HealthNotReadyStatus HealthNotReadyStatus health not ready status // // swagger:model healthNotReadyStatus type HealthNotReadyStatus struct { diff --git a/internal/httpclient/models/json_web_key_set.go b/internal/httpclient/models/json_web_key_set.go index 746750ad30..c4eccd4b90 100644 --- a/internal/httpclient/models/json_web_key_set.go +++ b/internal/httpclient/models/json_web_key_set.go @@ -13,7 +13,7 @@ import ( "github.com/go-openapi/swag" ) -// JSONWebKeySet JSONWebKeySet JSONWebKeySet json web key set +// JSONWebKeySet JSONWebKeySet JSONWebKeySet JSONWebKeySet json web key set // // swagger:model jsonWebKeySet type JSONWebKeySet struct { diff --git a/internal/httpclient/models/rule.go b/internal/httpclient/models/rule.go index 275ded0f54..b72a698519 100644 --- a/internal/httpclient/models/rule.go +++ b/internal/httpclient/models/rule.go @@ -13,7 +13,7 @@ import ( "github.com/go-openapi/swag" ) -// Rule swaggerRule is a single rule that will get checked on every HTTP request. +// Rule Rule swaggerRule is a single rule that will get checked on every HTTP request. // // swagger:model rule type Rule struct { diff --git a/internal/httpclient/models/rule_handler.go b/internal/httpclient/models/rule_handler.go index 3300ad7e1d..88cc88ae0a 100644 --- a/internal/httpclient/models/rule_handler.go +++ b/internal/httpclient/models/rule_handler.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag" ) -// RuleHandler RuleHandler rule handler +// RuleHandler RuleHandler RuleHandler rule handler // // swagger:model ruleHandler type RuleHandler struct { diff --git a/internal/httpclient/models/upstream.go b/internal/httpclient/models/upstream.go index 84c78d1bd6..524bf32c39 100644 --- a/internal/httpclient/models/upstream.go +++ b/internal/httpclient/models/upstream.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag" ) -// Upstream Upstream Upstream upstream +// Upstream Upstream Upstream Upstream upstream // // swagger:model Upstream type Upstream struct { diff --git a/pipeline/authn/authenticator_oauth2_client_credentials.go b/pipeline/authn/authenticator_oauth2_client_credentials.go index 7884e4237f..1e20dace75 100644 --- a/pipeline/authn/authenticator_oauth2_client_credentials.go +++ b/pipeline/authn/authenticator_oauth2_client_credentials.go @@ -3,12 +3,17 @@ package authn import ( "context" "encoding/json" + "fmt" "net/http" "net/url" + "strings" "time" + "github.com/dgraph-io/ristretto" "golang.org/x/oauth2" + "github.com/ory/x/logrusx" + "github.com/ory/x/httpx" "github.com/ory/oathkeeper/driver/configuration" @@ -22,14 +27,25 @@ import ( ) type AuthenticatorOAuth2Configuration struct { - Scopes []string `json:"required_scope"` - TokenURL string `json:"token_url"` - Retry *AuthenticatorOAuth2ClientCredentialsRetryConfiguration + Scopes []string `json:"required_scope"` + TokenURL string `json:"token_url"` + Retry *AuthenticatorOAuth2ClientCredentialsRetryConfiguration `json:"retry,omitempty"` + Cache clientCredentialsCacheConfig `json:"cache"` +} + +type clientCredentialsCacheConfig struct { + Enabled bool `json:"enabled"` + TTL string `json:"ttl"` + MaxTokens int `json:"max_tokens"` } type AuthenticatorOAuth2ClientCredentials struct { c configuration.Provider client *http.Client + + tokenCache *ristretto.Cache + cacheTTL *time.Duration + logger *logrusx.Logger } type AuthenticatorOAuth2ClientCredentialsRetryConfiguration struct { @@ -37,8 +53,8 @@ type AuthenticatorOAuth2ClientCredentialsRetryConfiguration struct { MaxWait string `json:"give_up_after"` } -func NewAuthenticatorOAuth2ClientCredentials(c configuration.Provider) *AuthenticatorOAuth2ClientCredentials { - return &AuthenticatorOAuth2ClientCredentials{c: c} +func NewAuthenticatorOAuth2ClientCredentials(c configuration.Provider, logger *logrusx.Logger) *AuthenticatorOAuth2ClientCredentials { + return &AuthenticatorOAuth2ClientCredentials{c: c, logger: logger} } func (a *AuthenticatorOAuth2ClientCredentials) GetID() string { @@ -86,9 +102,96 @@ func (a *AuthenticatorOAuth2ClientCredentials) Config(config json.RawMessage) (* timeout := time.Millisecond * duration a.client = httpx.NewResilientClientLatencyToleranceConfigurable(nil, timeout, maxWait) + if c.Cache.TTL != "" { + cacheTTL, err := time.ParseDuration(c.Cache.TTL) + if err != nil { + return nil, err + } + a.cacheTTL = &cacheTTL + } + + if a.tokenCache == nil { + maxTokens := int64(c.Cache.MaxTokens) + if maxTokens == 0 { + maxTokens = 1000 + } + a.logger.Debugf("Creating cache with max tokens: %d", maxTokens) + cache, err := ristretto.NewCache(&ristretto.Config{ + // This will hold about 1000 unique mutation responses. + NumCounters: 10 * maxTokens, + // Allocate a maximum amount of tokens to cache + MaxCost: maxTokens, + // This is a best-practice value. + BufferItems: 64, + // Use a static cost of 1, so we can limit the amount of tokens that can be stored + Cost: func(value interface{}) int64 { + return 1 + }, + }) + if err != nil { + return nil, err + } + + a.tokenCache = cache + } + return &c, nil } +func clientCredentialsConfigToKey(cc clientcredentials.Config) string { + return fmt.Sprintf("%s|%s|%s:%s", cc.TokenURL, strings.Join(cc.Scopes, " "), cc.ClientID, cc.ClientSecret) +} + +func (a *AuthenticatorOAuth2ClientCredentials) tokenFromCache(config *AuthenticatorOAuth2Configuration, clientCredentials clientcredentials.Config) *oauth2.Token { + if !config.Cache.Enabled { + return nil + } + + item, found := a.tokenCache.Get(clientCredentialsConfigToKey(clientCredentials)) + if !found { + return nil + } + + i, ok := item.([]byte) + if !ok { + return nil + } + + var v oauth2.Token + if err := json.Unmarshal(i, &v); err != nil { + return nil + } + return &v +} + +func (a *AuthenticatorOAuth2ClientCredentials) tokenToCache(config *AuthenticatorOAuth2Configuration, clientCredentials clientcredentials.Config, token oauth2.Token) { + if !config.Cache.Enabled { + return + } + + key := clientCredentialsConfigToKey(clientCredentials) + + if v, err := json.Marshal(token); err != nil { + return + } else if a.cacheTTL != nil { + // Allow up-to at most the cache TTL, otherwise use token expiry + ttl := token.Expiry.Sub(time.Now()) + if ttl > *a.cacheTTL { + ttl = *a.cacheTTL + } + + a.tokenCache.SetWithTTL(key, v, 1, ttl) + } else { + // If token has no expiry apply the same to the cache + ttl := time.Duration(0) + if !token.Expiry.IsZero() { + ttl = token.Expiry.Sub(time.Now()) + } + + a.tokenCache.SetWithTTL(key, v, 1, ttl) + } +} + func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error { cf, err := a.Config(config) if err != nil { @@ -110,7 +213,7 @@ func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, ses return errors.Wrapf(helper.ErrUnauthorized, err.Error()) } - c := &clientcredentials.Config{ + c := clientcredentials.Config{ ClientID: user, ClientSecret: password, Scopes: cf.Scopes, @@ -118,29 +221,37 @@ func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, ses AuthStyle: oauth2.AuthStyleInHeader, } - token, err := c.Token(context.WithValue( - r.Context(), - oauth2.HTTPClient, - c.Client, - )) + token := a.tokenFromCache(cf, c) - if err != nil { - if rErr, ok := err.(*oauth2.RetrieveError); ok { - switch httpStatusCode := rErr.Response.StatusCode; httpStatusCode { - case http.StatusServiceUnavailable: + if token == nil { + t, err := c.Token(context.WithValue( + r.Context(), + oauth2.HTTPClient, + c.Client, + )) + + if err != nil { + if rErr, ok := err.(*oauth2.RetrieveError); ok { + switch httpStatusCode := rErr.Response.StatusCode; httpStatusCode { + case http.StatusServiceUnavailable: + return errors.Wrapf(helper.ErrUpstreamServiceNotAvailable, err.Error()) + case http.StatusInternalServerError: + return errors.Wrapf(helper.ErrUpstreamServiceInternalServerError, err.Error()) + case http.StatusGatewayTimeout: + return errors.Wrapf(helper.ErrUpstreamServiceTimeout, err.Error()) + case http.StatusNotFound: + return errors.Wrapf(helper.ErrUpstreamServiceNotFound, err.Error()) + default: + return errors.Wrapf(helper.ErrUnauthorized, err.Error()) + } + } else { return errors.Wrapf(helper.ErrUpstreamServiceNotAvailable, err.Error()) - case http.StatusInternalServerError: - return errors.Wrapf(helper.ErrUpstreamServiceInternalServerError, err.Error()) - case http.StatusGatewayTimeout: - return errors.Wrapf(helper.ErrUpstreamServiceTimeout, err.Error()) - case http.StatusNotFound: - return errors.Wrapf(helper.ErrUpstreamServiceNotFound, err.Error()) - default: - return errors.Wrapf(helper.ErrUnauthorized, err.Error()) } - } else { - return errors.Wrapf(helper.ErrUpstreamServiceNotAvailable, err.Error()) } + + token = t + + a.tokenToCache(cf, c, *token) } if token.AccessToken == "" { diff --git a/pipeline/authn/authenticator_oauth2_client_credentials_cache_test.go b/pipeline/authn/authenticator_oauth2_client_credentials_cache_test.go new file mode 100644 index 0000000000..5e6108e9c0 --- /dev/null +++ b/pipeline/authn/authenticator_oauth2_client_credentials_cache_test.go @@ -0,0 +1,96 @@ +package authn + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + + "github.com/ory/oathkeeper/driver/configuration" + "github.com/ory/viper" + "github.com/ory/x/logrusx" +) + +func TestClientCredentialsCache(t *testing.T) { + viper.Reset() + + ts := httptest.NewServer(httprouter.New()) + + viper.Set("authenticators.oauth2_client_credentials.config.token_url", ts.URL+"/oauth2/token") + viper.Set("authenticators.oauth2_client_credentials.config.cache.enabled", true) + + logger := logrusx.New("", "") + c := configuration.NewViperProvider(logger) + a := NewAuthenticatorOAuth2ClientCredentials(c, logger) + assert.Equal(t, "oauth2_client_credentials", a.GetID()) + + config, err := a.Config(nil) + require.NoError(t, err) + + t.Run("method=tokenToCache", func(t *testing.T) { + t.Run("case=cache value", func(t *testing.T) { + token := oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(3600 * time.Second), + } + cc := clientcredentials.Config{ + ClientID: "id", + ClientSecret: "secret", + } + + a.tokenToCache(config, cc, token) + // wait for cache to save value + time.Sleep(time.Millisecond * 10) + + v := a.tokenFromCache(config, cc) + require.NotNil(t, v) + }) + + t.Run("case=cached invalid json value should not working", func(t *testing.T) { + cc := clientcredentials.Config{ + ClientID: "id", + ClientSecret: "secret", + } + + ok := a.tokenCache.Set(clientCredentialsConfigToKey(cc), []byte("invalid-json-string"), 1) + require.True(t, ok) + // wait cache to save value + time.Sleep(time.Millisecond * 10) + + v := a.tokenFromCache(config, cc) + require.Nil(t, v) + }) + + t.Run("case=cache with ttl", func(t *testing.T) { + token := oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(3600 * time.Second), + } + cc := clientcredentials.Config{ + ClientID: "id", + ClientSecret: "secret", + } + + config, _ := a.Config([]byte(`{ "cache": { "ttl": "100ms" } }`)) + a.tokenToCache(config, cc, token) + // wait cache to save value + time.Sleep(time.Millisecond * 10) + + v := a.tokenFromCache(config, cc) + require.NotNil(t, v) + + // wait cache to be expired + time.Sleep(time.Millisecond * 100) + v = a.tokenFromCache(config, cc) + require.Nil(t, v) + }) + }) + +} diff --git a/pipeline/authn/authenticator_oauth2_client_credentials_test.go b/pipeline/authn/authenticator_oauth2_client_credentials_test.go index df9055ec44..d032dd6c50 100644 --- a/pipeline/authn/authenticator_oauth2_client_credentials_test.go +++ b/pipeline/authn/authenticator_oauth2_client_credentials_test.go @@ -26,6 +26,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/ory/viper" @@ -38,13 +39,19 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/sjson" "github.com/ory/herodot" "github.com/ory/oathkeeper/helper" ) +func authOkDynamic(u string) *http.Request { + authOk := &http.Request{Header: http.Header{}} + authOk.SetBasicAuth(u, "secret") + + return authOk +} + func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { conf := internal.NewConfigurationWithDefaults() reg := internal.NewRegistry(conf) @@ -62,12 +69,14 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { upstreamFailure := &http.Request{Header: http.Header{}} upstreamFailure.SetBasicAuth("client", "secret") + calls := 0 for k, tc := range []struct { d string r *http.Request config json.RawMessage token_url string - setup func(*testing.T, *httprouter.Router) + setup func(*testing.T, *httprouter.Router, json.RawMessage) + check func(*testing.T, *httprouter.Router, json.RawMessage) expectErr error expectSession *authn.AuthenticationSession }{ @@ -84,7 +93,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUnauthorized, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.WriteError(w, r, helper.ErrUnauthorized) @@ -98,11 +107,184 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectSession: &authn.AuthenticationSession{Subject: "client"}, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) - h.Write(w, r, map[string]interface{}{"access_token": "foo-token"}) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 3600}) + }) + }, + }, + { + d: "passes due to enabled cache", + r: authOkDynamic("cache-case-1"), + expectErr: nil, + expectSession: &authn.AuthenticationSession{Subject: "cache-case-1"}, + config: json.RawMessage(`{ "cache": { "enabled": true } }`), + token_url: "", + setup: func(t *testing.T, h *httprouter.Router, c json.RawMessage) { + calls := 0 + h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + calls++ + if calls == 2 { + h := herodot.NewJSONWriter(logrus.New()) + h.WriteError(w, r, helper.ErrUpstreamServiceNotAvailable) + return + } + + h := herodot.NewJSONWriter(logrus.New()) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 3600}) + }) + + session := new(authn.AuthenticationSession) + err := a.Authenticate(authOkDynamic("cache-case-1"), session, c, nil) + + require.NoError(t, err) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + }, + }, + { + d: "passes due to enabled cache with expired cache", + r: authOkDynamic("cache-case-2"), + expectErr: nil, + expectSession: &authn.AuthenticationSession{Subject: "cache-case-2"}, + config: json.RawMessage(`{ "cache": { "enabled": true } }`), + token_url: "", + setup: func(t *testing.T, h *httprouter.Router, c json.RawMessage) { + h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + h := herodot.NewJSONWriter(logrus.New()) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 1}) + }) + + session := new(authn.AuthenticationSession) + err := a.Authenticate(authOkDynamic("cache-case-2"), session, c, nil) + + require.NoError(t, err) + + // wait for cache to expire + time.Sleep(time.Second * 1) + }, + }, + { + d: "passes due to enabled cache with no expiry", + r: authOkDynamic("cache-case-3"), + expectErr: nil, + expectSession: &authn.AuthenticationSession{Subject: "cache-case-3"}, + config: json.RawMessage(`{ "cache": { "enabled": true } }`), + token_url: "", + setup: func(t *testing.T, h *httprouter.Router, c json.RawMessage) { + calls := 0 + h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + calls++ + if calls == 2 { + h := herodot.NewJSONWriter(logrus.New()) + h.WriteError(w, r, helper.ErrUpstreamServiceNotAvailable) + return + } + + h := herodot.NewJSONWriter(logrus.New()) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 3600}) }) + + session := new(authn.AuthenticationSession) + err := a.Authenticate(authOkDynamic("cache-case-3"), session, c, nil) + + require.NoError(t, err) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + }, + }, + { + d: "passes with no shared cache between different token URLs", + r: authOkDynamic("cache-case-4"), + expectErr: nil, + expectSession: &authn.AuthenticationSession{Subject: "cache-case-4"}, + config: json.RawMessage(`{ "cache": { "enabled": true } }`), + token_url: "", + setup: func(t *testing.T, h *httprouter.Router, c json.RawMessage) { + calls = 0 + h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + calls++ + if calls == 3 { + h := herodot.NewJSONWriter(logrus.New()) + t.Errorf("expected only 2 calls to token endpoint this is number %d", calls) + h.WriteError(w, r, helper.ErrUpstreamServiceNotAvailable) + return + } + + h := herodot.NewJSONWriter(logrus.New()) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 3600}) + }) + + ts := httptest.NewServer(h) + + session := new(authn.AuthenticationSession) + // First request + err = a.Authenticate(authOkDynamic("cache-case-4"), session, json.RawMessage(`{ "token_url": "`+ts.URL+`/oauth2/token", "cache": { "enabled": true } }`), nil) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + + // Second request to test caching + err = a.Authenticate(authOkDynamic("cache-case-4"), session, json.RawMessage(`{ "token_url": "`+ts.URL+`/oauth2/token", "cache": { "enabled": true } }`), nil) + + require.NoError(t, err) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + }, + check: func(t *testing.T, router *httprouter.Router, message json.RawMessage) { + require.Equal(t, 2, calls, "expected a call to the token endpoint per token URL config") + }, + }, + { + d: "passes with no shared cache between different token URLs", + r: authOkDynamic("cache-case-5"), + expectErr: nil, + expectSession: &authn.AuthenticationSession{Subject: "cache-case-5"}, + config: json.RawMessage(`{ "cache": { "enabled": true } }`), + token_url: "", + setup: func(t *testing.T, h *httprouter.Router, c json.RawMessage) { + calls = 0 + h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + calls++ + if calls == 3 { + h := herodot.NewJSONWriter(logrus.New()) + t.Errorf("expected only 2 calls to token endpoint this is number %d", calls) + h.WriteError(w, r, helper.ErrUpstreamServiceNotAvailable) + return + } + + h := herodot.NewJSONWriter(logrus.New()) + h.Write(w, r, map[string]interface{}{"access_token": "foo-token", "expires_in": 3600}) + }) + + var authnConfig authn.AuthenticatorOAuth2Configuration + json.Unmarshal(c, &authnConfig) + + authnConfig.Scopes = []string{"some-scope"} + authnConfig.Cache.TTL = "6h" + scopeConfig, _ := json.Marshal(authnConfig) + + session := new(authn.AuthenticationSession) + // First request + err = a.Authenticate(authOkDynamic("cache-case-5"), session, scopeConfig, nil) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + + // Second request to check caching + err = a.Authenticate(authOkDynamic("cache-case-5"), session, scopeConfig, nil) + + require.NoError(t, err) + + // wait cache to save value + time.Sleep(time.Millisecond * 10) + }, + check: func(t *testing.T, router *httprouter.Router, message json.RawMessage) { + require.Equal(t, 2, calls, "expected a call to the token endpoint per scope config") }, }, { @@ -111,7 +293,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUpstreamServiceNotAvailable, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.WriteError(w, r, helper.ErrUpstreamServiceNotAvailable) @@ -124,7 +306,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUpstreamServiceTimeout, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.WriteError(w, r, helper.ErrUpstreamServiceTimeout) @@ -137,7 +319,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUpstreamServiceInternalServerError, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.WriteError(w, r, helper.ErrUpstreamServiceInternalServerError) @@ -150,7 +332,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUpstreamServiceNotFound, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/v1/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.Write(w, r, map[string]interface{}{"access_token": "foo-token"}) @@ -163,7 +345,7 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { expectErr: helper.ErrUnauthorized, config: json.RawMessage(`{}`), token_url: "", - setup: func(t *testing.T, h *httprouter.Router) { + setup: func(t *testing.T, h *httprouter.Router, _ json.RawMessage) { h.POST("/oauth2/token", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { h := herodot.NewJSONWriter(logrus.New()) h.WriteError(w, r, helper.ErrForbidden) @@ -174,10 +356,6 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { t.Run(fmt.Sprintf("method=authenticate/case=%d", k), func(t *testing.T) { router := httprouter.New() - if tc.setup != nil { - tc.setup(t, router) - } - ts := httptest.NewServer(router) if tc.token_url != "" { @@ -186,6 +364,10 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { tc.config, _ = sjson.SetBytes(tc.config, "token_url", ts.URL+"/oauth2/token") } + if tc.setup != nil { + tc.setup(t, router, tc.config) + } + session := new(authn.AuthenticationSession) err := a.Authenticate(tc.r, session, tc.config, nil) @@ -198,6 +380,10 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) { if tc.expectSession != nil { assert.EqualValues(t, tc.expectSession, session) } + + if tc.check != nil { + tc.check(t, router, tc.config) + } }) } diff --git a/spec/api.json b/spec/api.json index 12f180b5c4..0f86542be2 100755 --- a/spec/api.json +++ b/spec/api.json @@ -34,7 +34,7 @@ "type": "string" }, "Upstream": { - "description": "Upstream Upstream upstream", + "description": "Upstream Upstream Upstream upstream", "properties": { "preserve_host": { "description": "PreserveHost, if false (the default), tells ORY Oathkeeper to set the upstream request's Host header to the\nhostname of the API's upstream's URL. Setting this flag to true instructs ORY Oathkeeper not to do so.", @@ -87,6 +87,7 @@ "type": "object" }, "healthNotReadyStatus": { + "description": "HealthNotReadyStatus health not ready status", "properties": { "errors": { "additionalProperties": { @@ -184,7 +185,7 @@ "type": "object" }, "jsonWebKeySet": { - "description": "JSONWebKeySet JSONWebKeySet json web key set", + "description": "JSONWebKeySet JSONWebKeySet JSONWebKeySet json web key set", "properties": { "keys": { "description": "The value of the \"keys\" parameter is an array of JWK values. By\ndefault, the order of the JWK values within the array does not imply\nan order of preference among them, although applications of JWK Sets\ncan choose to assign a meaning to the order for their purposes, if\ndesired.", @@ -230,11 +231,11 @@ "$ref": "#/components/schemas/Upstream" } }, - "title": "swaggerRule is a single rule that will get checked on every HTTP request.", + "title": "Rule swaggerRule is a single rule that will get checked on every HTTP request.", "type": "object" }, "ruleHandler": { - "description": "RuleHandler rule handler", + "description": "RuleHandler RuleHandler rule handler", "properties": { "config": { "description": "Config contains the configuration for the handler. Please read the user\nguide for a complete list of each handler's available settings.", diff --git a/spec/swagger.json b/spec/swagger.json index 967162f4e1..cebbc436f2 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -267,7 +267,7 @@ "definitions": { "UUID": {"type": "string", "format": "uuid4"}, "Upstream": { - "description": "Upstream Upstream upstream", + "description": "Upstream Upstream Upstream upstream", "type": "object", "properties": { "preserve_host": { @@ -320,6 +320,7 @@ } }, "healthNotReadyStatus": { + "description": "HealthNotReadyStatus health not ready status", "type": "object", "properties": { "errors": { @@ -417,7 +418,7 @@ } }, "jsonWebKeySet": { - "description": "JSONWebKeySet JSONWebKeySet json web key set", + "description": "JSONWebKeySet JSONWebKeySet JSONWebKeySet json web key set", "type": "object", "properties": { "keys": { @@ -431,7 +432,7 @@ }, "rule": { "type": "object", - "title": "swaggerRule is a single rule that will get checked on every HTTP request.", + "title": "Rule swaggerRule is a single rule that will get checked on every HTTP request.", "properties": { "authenticators": { "description": "Authenticators is a list of authentication handlers that will try and authenticate the provided credentials.\nAuthenticators are checked iteratively from index 0 to n and if the first authenticator to return a positive\nresult will be the one used.\n\nIf you want the rule to first check a specific authenticator before \"falling back\" to others, have that authenticator\nas the first item in the array.", @@ -467,7 +468,7 @@ } }, "ruleHandler": { - "description": "RuleHandler rule handler", + "description": "RuleHandler RuleHandler rule handler", "type": "object", "properties": { "config": {