CVE-2021-32701 (Go, published 2021-06-22T20:15Z)
Description: value: ORY Oathkeeper is an Identity & Access Proxy (IAP) and Access Control Decision API that authorizes HTTP requests based on sets of Access Rules. When you make a request to an endpoint that requires the scope `foo` using an access token granted with that `foo` scope, introspection will be valid and that token will be cached. The problem comes when a second requests to an endpoint that requires the scope `bar` is made before the cache has expired. Whether the token is granted or not to the `bar` scope, introspection will be valid. A patch will be released with `v0.38.12-beta.1`. Per default, caching is disabled for the `oauth2_introspection` authenticator. When caching is disabled, this vulnerability does not exist. The cache is checked in [`func (a *AuthenticatorOAuth2Introspection) Authenticate(...)`](https://github.com/ory/oathkeeper/blob/6a31df1c3779425e05db1c2a381166b087cb29a4/pipeline/authn/authenticator_oauth2_introspection.go#L152). From [`tokenFromCache()`](https://github.com/ory/oathkeeper/blob/6a31df1c3779425e05db1c2a381166b087cb29a4/pipeline/authn/authenticator_oauth2_introspection.go#L97) it seems that it only validates the token expiration date, but ignores whether the token has or not the proper scopes. The vulnerability was introduced in PR #424. During review, we failed to require appropriate test coverage by the submitter which is the primary reason that the vulnerability passed the review process.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-863 \u2014 Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
Repository: https://github.com/ory/oathkeeper
Files changed:
.circleci/config.yml
Makefile
docs/docs/CHANGELOG.md
docs/docs/pipeline/authn.md
driver/configuration/provider.go
pipeline/authn/authenticator_oauth2_introspection.go
pipeline/authn/authenticator_oauth2_introspection_test.go
Commit message: Merge pull request from GHSA-qvp4-rpmr-xwrr

This patch addresses a security vulnerability which would bypass token claim validation once a token is in the cache.

For more information please refer to https://github.com/ory/oathkeeper/security/advisories/GHSA-qvp4-rpmr-xwrr

Vulnerable code:
      - run: go-acc -o coverage.txt ./... -- -failfast -timeout=20m
      - run: test -z "$CIRCLE_PR_NUMBER" && goveralls -service=circle-ci -coverprofile=coverage.txt -repotoken=$COVERALLS_REPO_TOKEN || echo "forks are not allowed to push to coveralls"
  these headers will be passed to upstream services.
  For example, an user containing scopes/branches inside an organization profile
  has some level of data addressed to him. In this case, the upstream service
  need to know that, and "filter" the data according to his "branch_id". The
  permission that is given to the user (and the remote authorizers manages) has
  a record of the "branch_id", for the following responses will be returned as
  status code 200 (if granted) and containing a header like X-Branch-Id.
  The upstream service receives the X-Branch-Id and does your thing.
  The configuration requires to configure a list of "allowed headers" returning
  from remote authorizer, that will be accepted in the pipeline.
  closes [#668](https://github.com/ory/oathkeeper/issues/668):
  JWT Claims added to error details field.
  closes [#630](https://github.com/ory/oathkeeper/issues/630):
  This makes `/` also a separator as well as the presumably default value of
  `.`. This allows using <\*> for matching only one path segment.
  closes [#511](https://github.com/ory/oathkeeper/issues/511):
  This change introduces a url_param config option for redirect error handler.
  If it contains a url paramter name, the redirect url will have this parameter
  set, containing the current url (from which Oathkeeper has redirected the
  user).
  size.
  This change makes sure the Content-Length doesn't get included in the response
  body and adds a test to prevent future regressions.
  closes [#417](https://github.com/ory/oathkeeper/issues/417):
  This patch introduces new configuration parameters that allow the hydrator
  mutator to cache requests.
  the body into account in its decision.
  The current remote_json authorizer does not have the ability to send the
  request body of the request to authorize. This means this cannot be taken into
  account while checking permissions.
  closes [#377](https://github.com/ory/oathkeeper/issues/377):
  This patch removes the `scope` key from the OAuth2 Introspection request body
  when a scope strategy other than `none` is set for the OAuth2 Introspection
  handler. If the scope strategy is `none`, the `scope` key is included in the
  body.
  closes [#324](https://github.com/ory/oathkeeper/issues/324):
  Add a new file 'Dockerfile-dc' which will primarily be used by Docker Compose
  to build docker images. Unlike the existing Dockerfile which depends on the
  Makefile to build the binary, this Dockerfile copies the source code and
  builds the binary.
  closes [#374](https://github.com/ory/oathkeeper/issues/374):
  Fixed link to Envoy configuration page and added link to AWS API Gateway.
  closes [#321](https://github.com/ory/oathkeeper/issues/321):
  This patch adds the ability to choose a matching strategy and adds a
  glob-based matching strategy to the available options (regex is still the
  default).
  closes [#336](https://github.com/ory/oathkeeper/issues/336):
  The subject_from modifier is a GJSON path that points to the `subject` field.
  Useful if the upstream API does not return a `{"subject": "..."}` format.
  closes [#335](https://github.com/ory/oathkeeper/issues/335):
  The extra_from modifier is a GJSON path that points to the extra field. Useful
  if the upstream API does not return a `{"subject": "...", "extra": "..."}`
  format.
  [#330](https://github.com/ory/oathkeeper/issues/330):
  Also updates the schemas to add missing cookie config element.
  closes [#323](https://github.com/ory/oathkeeper/issues/323):
  Previously, MIME matching respected the request's wildcards which lead to
  multiple handlers feeling responsible for a particular request. Now, wildcards
  coming from the HTTP Request itself are interpreted literally.
  Additionally, ORY Oathkeeper respected the X-Forwarded-For HTTP Header for
  matching remote IP addresses. This behavior is now turned off by default
  because clients were able to fake this header otherwise. It can explicitly be
  turned on by setting
  [#119](https://github.com/ory/oathkeeper/issues/119):
  This patch adds a new feature called error handlers. It allows to define the
  error handling logic globally and per rule. It is now possible, for example,
  to return a JSON response for `Accept: application/json` requests and a HTTP
  Redirect response for requests that are coming from a user.
  This also resolves several issues, as noted below:
  [#317](https://github.com/ory/oathkeeper/issues/317):
  Previously, some keys did not respect the values set in the environment
  variables.
  [#242](https://github.com/ory/oathkeeper/issues/242):
  Adds details such as the HTTP Method, User Agent, Subject, and other
  information to the logging output of both the reverse proxy and the decision
  API.
  closes [#283](https://github.com/ory/oathkeeper/issues/283):
  Remove health endpoints from the logs to make monitoring easier. Setting
  `log_level` to `trace` will show these calls.
  closes [#291](https://github.com/ory/oathkeeper/issues/291):
  A bug caused the rule matcher to not cache the regular expression result.
  [#280](https://github.com/ory/oathkeeper/issues/280):
  This patch automatically binds environment variables to configuration keys.
  This patch resolves several issues:
  [#257](https://github.com/ory/oathkeeper/issues/257):
  Add additional optional configuration to jwt and oauth2_introspection
  authenticators allowing to set from where (which header or query parameter)
  the token should be received. The configuration is a token_from field in
  per-rule-configuration, as described in a linked issue.
  closes [#266](https://github.com/ory/oathkeeper/issues/266):
  Adds the ability to modify rules with backwards compatibility.
  closes [#217](https://github.com/ory/oathkeeper/issues/217):
  This patch allows oathkeeper to re-load any changes made to the configuraiton
  file and/or the access rules to be reloaded without a restart.
  Some configuration keys like serve._, log._, profiling however require a
  restart.
  closes [#213](https://github.com/ory/oathkeeper/issues/213):
  This commit adds support for defining access rules in YAML format, in addition
  to existing JSON format.
  closes [#204](https://github.com/ory/oathkeeper/issues/204):
  There's no DSN in oathkeeper
  closes [#199](https://github.com/ory/oathkeeper/issues/199):
  Previously, negroni.With was mistakenly used to add middleware onto the stack.
  The proper method however is negroni.Use.
  This patch fixes the use of negroni.With and resolves issues around logging
  and the decisions endpoint.
  closes [#138](https://github.com/ory/oathkeeper/issues/138):
  Previously, the JWT authenticator only used the "scope" claim to retrieve
  scope values from a JWT. Now, "scp", "scope", "scopes" are supported as string
  arrays and strings separated by spaces.
  closes [#42](https://github.com/ory/oathkeeper/issues/42):
  This patch adds endpoint `/judge` to `oathkeeper serve api`. The `/judge`
  endpoint mimics the behavior of `oathkeeper serve proxy` but instead of
  forwarding the request to the upstream server, the endpoint answers directly
  with a HTTP response.
  The HTTP response returns status code 200 if the request should be allowed and
  any other status code (e.g. 401, 403) if not.
  Assuming you are making the following request:
  a server handled proxy requests.
  It will help prevent a case of unexpected closing a client connection if an
  upstream request is executing more than default timeout.
  closes [#101](https://github.com/ory/oathkeeper/issues/101):
  This patch improves compatibility with ORY Hydra 1.0.0-beta.8 and updates
  vendored dependencies.
  closes [#38](https://github.com/ory/oathkeeper/issues/38):
  Previously, PUT and POST did not result in errors (409) when non-existing
  resources were modified, or existing resources were created. This patch
  resolves that.
  closes [#73](https://github.com/ory/oathkeeper/issues/73):
  This patch resolves an issue where updates would not properly propagate. This
  caused deleted rules to still be available in the proxy.
  closes [#71](https://github.com/ory/oathkeeper/issues/71):
  This patch adds another ID Token signing algorithm (HS256) which is easier to
  set up as it does not rely on ORY Hydra but instead on a shared secret.
  Additionally the ability to specify which ID Token singing algorithm to use
  has been added. Environmental variables to configure the behvaiour have been
  added as well.
func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string) (*AuthenticatorOAuth2IntrospectionResult, bool) {
		return nil, false
		return nil, false
	i := item.(*AuthenticatorOAuth2IntrospectionResult)
	expires := time.Unix(i.Expires, 0)
	if expires.Before(time.Now()) {
		a.tokenCache.Del(token)
		return nil, false
	return i, true
func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string) {
	tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.scope_strategy")
	i, ok := a.tokenFromCache(cf, token)
	if !ok {
		body := url.Values{"token": {token}}
		if len(i.TokenUse) > 0 && i.TokenUse != "access_token" {
			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Use of introspected token is not an access token but \"%s\"", i.TokenUse)))
		}
		if !i.Active {
			return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token i says token is not active"))
		}
		for _, audience := range cf.Audience {
			if !stringslice.Has(i.Audience, audience) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
			}
		if len(cf.Issuers) > 0 {
			if !stringslice.Has(cf.Issuers, i.Issuer) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
			}
		if ss != nil {
			for _, scope := range cf.Scopes {
				if !ss(strings.Split(i.Scope, " "), scope) {
					return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
				}
		if len(i.Extra) == 0 {
			i.Extra = map[string]interface{}{}
		}
		i.Extra["username"] = i.Username
		i.Extra["client_id"] = i.ClientID
		i.Extra["scope"] = i.Scope
		if len(i.Audience) != 0 {
			i.Extra["aud"] = i.Audience
		}
		a.tokenToCache(cf, i, token)
		cache, _ := ristretto.NewCache(&ristretto.Config{
			MaxCost: int64(c.Cache.MaxCost),


Fixed code:
      - run: go-acc -o coverage.out ./... -- -failfast -timeout=20m
      - run: |
          bash <(curl -s https://codecov.io/bash)
node_modules: package.json package-lock.json
		npm i

  these headers will be passed to upstream services. For example, an user
  containing scopes/branches inside an organization profile has some level of
  data addressed to him. In this case, the upstream service need to know that,
  and "filter" the data according to his "branch_id". The permission that is
  given to the user (and the remote authorizers manages) has a record of the
  "branch_id", for the following responses will be returned as status code 200
  (if granted) and containing a header like X-Branch-Id. The upstream service
  receives the X-Branch-Id and does your thing. The configuration requires to
  configure a list of "allowed headers" returning from remote authorizer, that
  will be accepted in the pipeline.


  closes [#668](https://github.com/ory/oathkeeper/issues/668): JWT Claims added
  to error details field.
  closes [#630](https://github.com/ory/oathkeeper/issues/630): This makes `/`
  also a separator as well as the presumably default value of `.`. This allows
  using <\*> for matching only one path segment.
  closes [#511](https://github.com/ory/oathkeeper/issues/511): This change
  introduces a url_param config option for redirect error handler. If it
  contains a url paramter name, the redirect url will have this parameter set,
  containing the current url (from which Oathkeeper has redirected the user).
  size. This change makes sure the Content-Length doesn't get included in the
  response body and adds a test to prevent future regressions.
  closes [#417](https://github.com/ory/oathkeeper/issues/417): This patch
  introduces new configuration parameters that allow the hydrator mutator to
  cache requests.
  the body into account in its decision. The current remote_json authorizer does
  not have the ability to send the request body of the request to authorize.
  This means this cannot be taken into account while checking permissions.
  closes [#377](https://github.com/ory/oathkeeper/issues/377): This patch
  removes the `scope` key from the OAuth2 Introspection request body when a
  scope strategy other than `none` is set for the OAuth2 Introspection handler.
  If the scope strategy is `none`, the `scope` key is included in the body.
  closes [#324](https://github.com/ory/oathkeeper/issues/324): Add a new file
  'Dockerfile-dc' which will primarily be used by Docker Compose to build docker
  images. Unlike the existing Dockerfile which depends on the Makefile to build
  the binary, this Dockerfile copies the source code and builds the binary.
  closes [#374](https://github.com/ory/oathkeeper/issues/374): Fixed link to
  Envoy configuration page and added link to AWS API Gateway.
  closes [#321](https://github.com/ory/oathkeeper/issues/321): This patch adds
  the ability to choose a matching strategy and adds a glob-based matching
  strategy to the available options (regex is still the default).
  closes [#336](https://github.com/ory/oathkeeper/issues/336): The subject_from
  modifier is a GJSON path that points to the `subject` field. Useful if the
  upstream API does not return a `{"subject": "..."}` format.
  closes [#335](https://github.com/ory/oathkeeper/issues/335): The extra_from
  modifier is a GJSON path that points to the extra field. Useful if the
  upstream API does not return a `{"subject": "...", "extra": "..."}` format.
  [#330](https://github.com/ory/oathkeeper/issues/330): Also updates the schemas
  to add missing cookie config element.
  closes [#323](https://github.com/ory/oathkeeper/issues/323): Previously, MIME
  matching respected the request's wildcards which lead to multiple handlers
  feeling responsible for a particular request. Now, wildcards coming from the
  HTTP Request itself are interpreted literally. Additionally, ORY Oathkeeper
  respected the X-Forwarded-For HTTP Header for matching remote IP addresses.
  This behavior is now turned off by default because clients were able to fake
  this header otherwise. It can explicitly be turned on by setting
  [#119](https://github.com/ory/oathkeeper/issues/119): This patch adds a new
  feature called error handlers. It allows to define the error handling logic
  globally and per rule. It is now possible, for example, to return a JSON
  response for `Accept: application/json` requests and a HTTP Redirect response
  for requests that are coming from a user. This also resolves several issues,
  as noted below:
  [#317](https://github.com/ory/oathkeeper/issues/317): Previously, some keys
  did not respect the values set in the environment variables.
  [#242](https://github.com/ory/oathkeeper/issues/242): Adds details such as the
  HTTP Method, User Agent, Subject, and other information to the logging output
  of both the reverse proxy and the decision API.
  closes [#283](https://github.com/ory/oathkeeper/issues/283): Remove health
  endpoints from the logs to make monitoring easier. Setting `log_level` to
  `trace` will show these calls.
  closes [#291](https://github.com/ory/oathkeeper/issues/291): A bug caused the
  rule matcher to not cache the regular expression result.
  [#280](https://github.com/ory/oathkeeper/issues/280): This patch automatically
  binds environment variables to configuration keys. This patch resolves several
  issues:
  [#257](https://github.com/ory/oathkeeper/issues/257): Add additional optional
  configuration to jwt and oauth2_introspection authenticators allowing to set
  from where (which header or query parameter) the token should be received. The
  configuration is a token_from field in per-rule-configuration, as described in
  a linked issue.
  closes [#266](https://github.com/ory/oathkeeper/issues/266): Adds the ability
  to modify rules with backwards compatibility.
  closes [#217](https://github.com/ory/oathkeeper/issues/217): This patch allows
  oathkeeper to re-load any changes made to the configuraiton file and/or the
  access rules to be reloaded without a restart. Some configuration keys like
  serve._, log._, profiling however require a restart.
  closes [#213](https://github.com/ory/oathkeeper/issues/213): This commit adds
  support for defining access rules in YAML format, in addition to existing JSON
  format.
  closes [#204](https://github.com/ory/oathkeeper/issues/204): There's no DSN in
  oathkeeper
  closes [#199](https://github.com/ory/oathkeeper/issues/199): Previously,
  negroni.With was mistakenly used to add middleware onto the stack. The proper
  method however is negroni.Use. This patch fixes the use of negroni.With and
  resolves issues around logging and the decisions endpoint.
  closes [#138](https://github.com/ory/oathkeeper/issues/138): Previously, the
  JWT authenticator only used the "scope" claim to retrieve scope values from a
  JWT. Now, "scp", "scope", "scopes" are supported as string arrays and strings
  separated by spaces.
  closes [#42](https://github.com/ory/oathkeeper/issues/42): This patch adds
  endpoint `/judge` to `oathkeeper serve api`. The `/judge` endpoint mimics the
  behavior of `oathkeeper serve proxy` but instead of forwarding the request to
  the upstream server, the endpoint answers directly with a HTTP response. The
  HTTP response returns status code 200 if the request should be allowed and any
  other status code (e.g. 401, 403) if not. Assuming you are making the
  following request:
  a server handled proxy requests. It will help prevent a case of unexpected
  closing a client connection if an upstream request is executing more than
  default timeout.
  closes [#101](https://github.com/ory/oathkeeper/issues/101): This patch
  improves compatibility with ORY Hydra 1.0.0-beta.8 and updates vendored
  dependencies.
  closes [#38](https://github.com/ory/oathkeeper/issues/38): Previously, PUT and
  POST did not result in errors (409) when non-existing resources were modified,
  or existing resources were created. This patch resolves that.
  closes [#73](https://github.com/ory/oathkeeper/issues/73): This patch resolves
  an issue where updates would not properly propagate. This caused deleted rules
  to still be available in the proxy.
  closes [#71](https://github.com/ory/oathkeeper/issues/71): This patch adds
  another ID Token signing algorithm (HS256) which is easier to set up as it
  does not rely on ORY Hydra but instead on a shared secret. Additionally the
  ability to specify which ID Token singing algorithm to use has been added.
  Environmental variables to configure the behvaiour have been added as well.

Please note that caching will not be used if the scope strategy is `none` and
`required_scope` is not empty. In that case, the configured introspection URL
will always be called and is expected to check if the scope is valid or not.


	"github.com/ory/fosite"

func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string, ss fosite.ScopeStrategy) *AuthenticatorOAuth2IntrospectionResult {
		return nil
	}

	if ss == nil && len(config.Scopes) > 0 {
		return nil
		return nil
	i, ok := item.(*AuthenticatorOAuth2IntrospectionResult)
	if !ok {
		return nil
	return i
func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string, ss fosite.ScopeStrategy) {
	if ss == nil && len(config.Scopes) > 0 {
		return
	}

	_ = tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.config.scope_strategy")
	i := a.tokenFromCache(cf, token, ss)
	// If the token can not be found, and the scope strategy is nil, and the required scope list
	// is not empty, then we can not use the cache.
	if i == nil {
		body := url.Values{"token": {token}}

	}
	if len(i.TokenUse) > 0 && i.TokenUse != "access_token" {
		return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Use of introspected token is not an access token but \"%s\"", i.TokenUse)))
	}
	if !i.Active {
		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token is not active"))
	}
	if i.Expires > 0 && time.Unix(i.Expires, 0).Before(time.Now()) {
		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token expired"))
	}

	for _, audience := range cf.Audience {
		if !stringslice.Has(i.Audience, audience) {
			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
	}
	if len(cf.Issuers) > 0 {
		if !stringslice.Has(cf.Issuers, i.Issuer) {
			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
	}
	if ss != nil {
		for _, scope := range cf.Scopes {
			if !ss(strings.Split(i.Scope, " "), scope) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
	}
	a.tokenToCache(cf, i, token, ss)
	if len(i.Extra) == 0 {
		i.Extra = map[string]interface{}{}
	}
	i.Extra["username"] = i.Username
	i.Extra["client_id"] = i.ClientID
	i.Extra["scope"] = i.Scope
	if len(i.Audience) != 0 {
		i.Extra["aud"] = i.Audience
		cost := int64(c.Cache.MaxCost)
		if cost == 0 {
			cost = 100000000
		}
		cache, err := ristretto.NewCache(&ristretto.Config{
			MaxCost: cost,
		if err != nil {
			return nil, nil, err
		}
	"sync"
	"time"

	"github.com/ory/x/assertx"
	t.Run("method=authenticate-with-cache", func(t *testing.T) {
		viper.Set("authenticators.oauth2_introspection.config.cache.enabled", true)
		t.Cleanup(func() {
			viper.Set("authenticators.oauth2_introspection.config.cache.enabled", false)
		})

		var didNotUseCache sync.WaitGroup

		setup := func(t *testing.T, config string) []byte {
			router := httprouter.New()
			router.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
				defer didNotUseCache.Done()
				require.NoError(t, r.ParseForm())
				switch r.Form.Get("token") {
				case "inactive-scope-b":
					require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{
						Active: false,
					}))
				case "another-active-scope-a":
					fallthrough
				case "active-scope-a":
					if r.Form.Get("scope") != "" && r.Form.Get("scope") != "scope-a" {
						require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{
							Active: false,
						}))
						return
					}
					require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{
						Active:   true,
						Scope:    "scope-a",
						Subject:  "subject",
						Audience: []string{"audience"},
						Issuer:   "foo",
						Username: "username",
						Expires:  time.Now().Add(time.Second).Unix(),
						Extra:    map[string]interface{}{"extra": "foo"},
					}))
				case "refresh-token":
					require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{
						Active:   true,
						Scope:    "scope-a",
						Subject:  "subject",
						Audience: []string{"audience"},
						Issuer:   "foo",
						Username: "username",
						TokenUse: "refresh_token",
						Extra:    map[string]interface{}{"extra": "foo"},
					}))
				default:
					require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{
						Active: false,
					}))
				}
			})
			ts := httptest.NewServer(router)
			t.Cleanup(ts.Close)

			config, err = sjson.Set(config, "introspection_url", ts.URL+"/oauth2/introspect")
			require.NoError(t, err)
			config, err = sjson.Set(config, "pre_authorization.token_url", ts.URL+"/oauth2/token")
			require.NoError(t, err)

			return []byte(config)
		}

		t.Run("case=with none scope strategy", func(t *testing.T) {
			viper.Set("authenticators.oauth2_introspection.config.scope_strategy", "none")
			r := &http.Request{Header: http.Header{"Authorization": {"bearer active-scope-a"}}}
			expected := new(AuthenticationSession)
			t.Run("case=initial request succeeds", func(t *testing.T) {
				config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)

				didNotUseCache.Add(1)
				err = a.Authenticate(r, expected, config, nil)
				didNotUseCache.Wait()
				require.NoError(t, err)
			})

			// We expect to use the cache here because we are not interested to validate the scope. Usually we would
			// expect to make the upstream call if the upstream has to validate the scope.
			t.Run("case=second request does use cache because no scope was requested and strategy is nil", func(t *testing.T) {
				config := setup(t, `{ "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				sess := new(AuthenticationSession)

				err = a.Authenticate(r, sess, config, nil)
				didNotUseCache.Wait() // Would result in a panic if wg.done was called!
				require.NoError(t, err)
				assertx.EqualAsJSON(t, expected, sess)
			})

			t.Run("case=second request does not use cache because scope strategy is disabled and scope was requested request succeeds", func(t *testing.T) {
				config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				sess := new(AuthenticationSession)

				didNotUseCache.Add(1)
				err = a.Authenticate(r, sess, config, nil)
				didNotUseCache.Wait()
				require.NoError(t, err)
				assertx.EqualAsJSON(t, expected, sess)
			})

			t.Run("case=request fails because we requested a scope which the upstream does not validate", func(t *testing.T) {
				config := setup(t, `{ "required_scope": ["scope-b"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				sess := new(AuthenticationSession)

				didNotUseCache.Add(1)
				err = a.Authenticate(r, sess, config, nil)
				didNotUseCache.Wait()
				require.Error(t, err)
			})
		})

		t.Run("case=does not use cache for refresh tokens", func(t *testing.T) {
			for _, strategy := range []string{"wildcard", "none"} {
				t.Run("scope_strategy="+strategy, func(t *testing.T) {
					viper.Set("authenticators.oauth2_introspection.config.scope_strategy", strategy)
					r := &http.Request{Header: http.Header{"Authorization": {"bearer refresh_token"}}}
					expected := new(AuthenticationSession)

					// The initial request
					config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)

					// Also doesn't use the cache the second time
					didNotUseCache.Add(2)
					require.Error(t, a.Authenticate(r, expected, config, nil))
					require.Error(t, a.Authenticate(r, expected, config, nil))
					didNotUseCache.Wait()
				})
			}
		})

		t.Run("case=with a scope scope strategy", func(t *testing.T) {
			viper.Set("authenticators.oauth2_introspection.config.scope_strategy", "wildcard")
			r := &http.Request{Header: http.Header{"Authorization": {"bearer another-active-scope-a"}}}
			expected := new(AuthenticationSession)

			// The initial request
			config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)

			didNotUseCache.Add(1)
			require.NoError(t, a.Authenticate(r, expected, config, nil))
			didNotUseCache.Wait()

			t.Run("case=request succeeds and uses the cache", func(t *testing.T) {
				config := setup(t, `{ "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				sess := new(AuthenticationSession)

				err = a.Authenticate(r, sess, config, nil)
				didNotUseCache.Wait()
				require.NoError(t, err)
				assertx.EqualAsJSON(t, expected, sess)
			})

			t.Run("case=request the initial request which also passes", func(t *testing.T) {
				config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				sess := new(AuthenticationSession)

				err = a.Authenticate(r, sess, config, nil)
				didNotUseCache.Wait()
				require.NoError(t, err)
				assertx.EqualAsJSON(t, expected, sess)
			})

			t.Run("case=requests a scope the token does not have", func(t *testing.T) {
				require.Error(t, a.Authenticate(r, new(AuthenticationSession),
					setup(t, `{ "required_scope": ["scope-b"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`),
					nil))
				didNotUseCache.Wait()
			})

			t.Run("case=requests an audience which the token does not have", func(t *testing.T) {
				require.Error(t, a.Authenticate(r, new(AuthenticationSession),
					setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["not-audience"] }`),
					nil))
				didNotUseCache.Wait()
			})

			t.Run("case=does not trust the issuer", func(t *testing.T) {
				require.Error(t, a.Authenticate(r, new(AuthenticationSession),
					setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["not-foo", "bar"], "target_audience": ["audience"] }`),
					nil))
				didNotUseCache.Wait()
			})

			t.Run("case=respects the expiry time", func(t *testing.T) {
				setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`)
				require.NoError(t, a.Authenticate(r, new(AuthenticationSession), config, nil))
				time.Sleep(time.Second)
				require.Error(t, a.Authenticate(r, new(AuthenticationSession), config, nil))
				didNotUseCache.Wait()
			})
		})
	})