//  [go]
func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
	cf, client, err := a.Config(config)
	if err != nil {
		return err
	}

	token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
	if token == "" {
		return errors.WithStack(ErrAuthenticatorNotResponsible)
	}

	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 ss == nil {
			body.Add("scope", strings.Join(cf.Scopes, " "))
		}

		introspectReq, err := http.NewRequest(http.MethodPost, cf.IntrospectionURL, strings.NewReader(body.Encode()))
		if err != nil {
			return errors.WithStack(err)
		}

		for key, value := range cf.IntrospectionRequestHeaders {
			introspectReq.Header.Set(key, value)
		}
		// set/override the content-type header
		introspectReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		// add tracing
		closeSpan := a.traceRequest(r.Context(), introspectReq)

		resp, err := client.Do(introspectReq.WithContext(r.Context()))

		// close the span so it represents just the http request
		closeSpan()
		if err != nil {
			return errors.WithStack(err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return errors.Errorf("Introspection returned status code %d but expected %d", resp.StatusCode, http.StatusOK)
		}

		if err := json.NewDecoder(resp.Body).Decode(&i); err != nil {
			return errors.WithStack(err)
		}
	}

	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
	}

	session.Subject = i.Subject
	session.Extra = i.Extra

	return nil
}