Repository: avenga/couper
Pull request #517 (2022-06-07 15:08:30)

Token request
Describe your changes!

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/config/backend.go b/config/backend.go
index c7d9ec57..6bf6e20a 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -24,9 +24,6 @@ type Backend struct {
 	Name                   string   `hcl:"name,label,optional"`
 	OpenAPI                *OpenAPI `hcl:"openapi,block"`
 	Remain                 hcl.Body `hcl:",remain"`
-
-	// explicit configuration on load
-	OAuth2 *OAuth2ReqAuth
 }
 
 // Reference implements the <BackendReference> interface.
diff --git a/config/body/body.go b/config/body/body.go
index bc882916..679c1d7d 100644
--- a/config/body/body.go
+++ b/config/body/body.go
@@ -34,6 +34,14 @@ func NewContentWithAttr(attr *hcl.Attribute) *hcl.BodyContent {
 	}}
 }
 
+func RenameAttribute(content *hcl.BodyContent, old, new string) {
+	if attr, ok := content.Attributes[old]; ok {
+		attr.Name = new
+		content.Attributes[new] = attr
+		delete(content.Attributes, old)
+	}
+}
+
 func (e *Body) Content(_ *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
 	return e.content, nil
 }
diff --git a/config/configload/backend.go b/config/configload/backend.go
index 1a053c8b..c9a5360d 100644
--- a/config/configload/backend.go
+++ b/config/configload/backend.go
@@ -113,7 +113,22 @@ func PrepareBackend(helper *helper, attrName, attrValue string, block config.Inl
 	}
 
 	if oauth2Backend != nil {
-		wrapped := wrapOauth2Backend(oauth2Backend)
+		wrapped := wrapBlock(oauth2, "", oauth2Backend)
+		backendBody = hclbody.MergeBodies(backendBody, wrapped)
+	}
+
+	// watch out for beta_token_request blocks and nested backend definitions
+	tokenRequestBackends, err := newTokenRequestBackend(helper, backendBody)
+	if err != nil {
+		return nil, err
+	}
+
+	if tokenRequestBackends == nil {
+		return backendBody, nil
+	}
+
+	for label, tokenRequestBackend := range tokenRequestBackends {
+		wrapped := wrapBlock(tokenRequest, label, tokenRequestBackend)
 		backendBody = hclbody.MergeBodies(backendBody, wrapped)
 	}
 
@@ -169,24 +184,73 @@ func newOAuthBackend(helper *helper, parent hcl.Body) (hcl.Body, error) {
 	return PrepareBackend(helper, "", conf.TokenEndpoint, conf)
 }
 
-func wrapOauth2Backend(content hcl.Body) hcl.Body {
-	b := hclbody.New(&hcl.BodyContent{
+// newTokenRequestBackend prepares a nested backend within each backend-tokenRequest block.
+// TODO: Check a possible circular dependency with given parent backend(s).
+func newTokenRequestBackend(helper *helper, parent hcl.Body) (map[string]hcl.Body, error) {
+	innerContent, err := contentByType(tokenRequest, parent)
+	if err != nil {
+		return nil, err
+	}
+
+	tokenRequestBlocks := innerContent.Blocks.OfType(tokenRequest)
+	if len(tokenRequestBlocks) == 0 {
+		return nil, nil
+	}
+
+	tokenRequestBackends := make(map[string]hcl.Body)
+	// beta_token_request block exists, read out backend configuration
+	for _, tokenRequestBlock := range tokenRequestBlocks {
+		label := ""
+		if len(tokenRequestBlock.Labels) > 0 {
+			label = tokenRequestBlock.Labels[0]
+		}
+		tokenRequestBody := tokenRequestBlock.Body
+		conf := &config.TokenRequest{Name: label}
+		if diags := gohcl.DecodeBody(tokenRequestBody, helper.context, conf); diags.HasErrors() {
+			return nil, diags
+		}
+
+		content, _, diags := conf.Remain.PartialContent(conf.Schema(true))
+		if diags.HasErrors() {
+			return nil, diags
+		}
+
+		if err = verifyBodyAttributes(tokenRequest, content); err != nil {
+			return nil, err
+		}
+
+		be, berr := PrepareBackend(helper, "", conf.URL, conf)
+		if berr != nil {
+			return nil, berr
+		}
+		tokenRequestBackends[label] = be
+	}
+
+	return tokenRequestBackends, nil
+}
+
+func wrapBlock(blockType, label string, content hcl.Body) hcl.Body {
+	var labels []string
+	if label != "" {
+		labels = append(labels, label)
+	}
+	return hclbody.New(&hcl.BodyContent{
 		Blocks: []*hcl.Block{
 			{
-				Type: oauth2,
-				Body: newBackendBlock(content),
+				Body:   newBlock(backend, content),
+				Labels: labels,
+				Type:   blockType,
 			},
 		},
 	})
-	return b
 }
 
-func newBackendBlock(content hcl.Body) hcl.Body {
+func newBlock(blockType string, content hcl.Body) hcl.Body {
 	return hclbody.New(&hcl.BodyContent{
 		Blocks: []*hcl.Block{
 			{
-				Type: backend,
 				Body: content,
+				Type: blockType,
 			},
 		},
 	})
diff --git a/config/configload/endpoint.go b/config/configload/endpoint.go
index 06226a92..a6bb7787 100644
--- a/config/configload/endpoint.go
+++ b/config/configload/endpoint.go
@@ -136,12 +136,12 @@ func refineEndpoints(helper *helper, endpoints config.Endpoints, check bool) err
 				return diags
 			}
 
-			if err := verifyBodyAttributes(content); err != nil {
+			if err = verifyBodyAttributes(request, content); err != nil {
 				return err
 			}
 
-			renameAttribute(content, "headers", "set_request_headers")
-			renameAttribute(content, "query_params", "set_query_params")
+			hclbody.RenameAttribute(content, "headers", "set_request_headers")
+			hclbody.RenameAttribute(content, "query_params", "set_query_params")
 
 			reqConfig.Remain = hclbody.MergeBodies(leftOvers, hclbody.New(content))
 
@@ -203,11 +203,3 @@ func getWebsocketsConfig(proxyConfig *config.Proxy) (bool, hcl.Body, error) {
 
 	return len(content.Blocks) > 0, nil, nil
 }
-
-func renameAttribute(content *hcl.BodyContent, old, new string) {
-	if attr, ok := content.Attributes[old]; ok {
-		attr.Name = new
-		content.Attributes[new] = attr
-		delete(content.Attributes, old)
-	}
-}
diff --git a/config/configload/helper.go b/config/configload/helper.go
index 59d461ce..25996ea7 100644
--- a/config/configload/helper.go
+++ b/config/configload/helper.go
@@ -124,7 +124,7 @@ func (h *helper) resolveBackendDeps() (uniqueItems []string, err error) {
 	// built up deps
 	refPtr := map[string]*sequence.Item{}
 	for name := range refs {
-		parent := &sequence.Item{Name: name}
+		parent := sequence.NewBackendItem(name)
 		refPtr[name] = parent
 	}
 
@@ -141,7 +141,7 @@ func (h *helper) resolveBackendDeps() (uniqueItems []string, err error) {
 			if be, exist := refPtr[r]; exist {
 				p.Add(be)
 			} else {
-				p.Add(&sequence.Item{Name: r})
+				p.Add(sequence.NewBackendItem(r))
 			}
 			defs = append(defs, p)
 		}
@@ -176,36 +176,46 @@ func (h *helper) collectBackendDeps(refs map[string][]string) {
 	for name, b := range h.defsBackends {
 		refs[name] = nil
 		content, _, _ := b.PartialContent(&hcl.BodySchema{
-			Blocks: []hcl.BlockHeaderSchema{{Type: oauth2}}},
+			Blocks: []hcl.BlockHeaderSchema{
+				{Type: oauth2},
+				{Type: tokenRequest, LabelNames: []string{"name"}, LabelOptional: true},
+			}},
 		)
 		oaBlocks := content.Blocks.OfType(oauth2)
-		for _, ob := range oaBlocks {
-			osb, ok := ob.Body.(*hclsyntax.Body)
-			if !ok {
-				continue
-			}
+		h.collectFromBlocks(oaBlocks, name, refs)
+		trBlocks := content.Blocks.OfType(tokenRequest)
+		h.collectFromBlocks(trBlocks, name, refs)
+	}
+}
 
-			for _, be := range osb.Attributes {
-				if be.Name == backend {
-					val, _ := be.Expr.Value(envContext)
-					refs[name] = append(refs[name], val.AsString())
-					break
-				}
+func (h *helper) collectFromBlocks(authorizerBlocks hcl.Blocks, name string, refs map[string][]string) {
+	for _, ab := range authorizerBlocks {
+		asb, ok := ab.Body.(*hclsyntax.Body)
+		if !ok {
+			continue
+		}
+
+		for _, be := range asb.Attributes {
+			if be.Name == backend {
+				val, _ := be.Expr.Value(envContext)
+				refs[name] = append(refs[name], val.AsString())
+				break
 			}
+		}
 
-			for _, block := range osb.Blocks {
-				if block.Type != backend {
-					continue
-				}
-				if len(block.Labels) > 0 {
-					refs[name] = append(refs[name], block.Labels[0])
-				}
+		for _, block := range asb.Blocks {
+			if block.Type != backend {
+				continue
+			}
+			if len(block.Labels) > 0 {
+				refs[name] = append(refs[name], block.Labels[0])
+			}
 
-				for _, subBlock := range block.Body.Blocks {
-					if subBlock.Type == oauth2 {
-						h.collectBackendDeps(refs)
-						break
-					}
+			for _, subBlock := range block.Body.Blocks {
+				switch subBlock.Type {
+				case oauth2, tokenRequest:
+					h.collectBackendDeps(refs)
+					break
 				}
 			}
 		}
diff --git a/config/configload/load.go b/config/configload/load.go
index 635985d4..cb4b98c9 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -25,21 +25,23 @@ import (
 )
 
 const (
-	api          = "api"
-	backend      = "backend"
-	defaults     = "defaults"
-	definitions  = "definitions"
-	endpoint     = "endpoint"
-	environment  = "environment"
-	errorHandler = "error_handler"
-	files        = "files"
-	nameLabel    = "name"
-	oauth2       = "oauth2"
-	proxy        = "proxy"
-	request      = "request"
-	server       = "server"
-	settings     = "settings"
-	spa          = "spa"
+	api             = "api"
+	backend         = "backend"
+	defaults        = "defaults"
+	definitions     = "definitions"
+	endpoint        = "endpoint"
+	environment     = "environment"
+	environmentVars = "environment_variables"
+	errorHandler    = "error_handler"
+	files           = "files"
+	nameLabel       = "name"
+	oauth2          = "oauth2"
+	proxy           = "proxy"
+	request         = "request"
+	server          = "server"
+	settings        = "settings"
+	spa             = "spa"
+	tokenRequest    = "beta_token_request"
 	// defaultNameLabel maps the hcl label attr 'name'.
 	defaultNameLabel = "default"
 )
diff --git a/config/configload/merge.go b/config/configload/merge.go
index 516b7779..abc92260 100644
--- a/config/configload/merge.go
+++ b/config/configload/merge.go
@@ -472,18 +472,18 @@ func mergeDefaults(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
 		for _, block := range body.Blocks {
 			if block.Type == defaults {
 				for name, attr := range block.Body.Attributes {
-					if name == "environment_variables" {
+					if name == environmentVars {
 						v, err := eval.Value(nil, attr.Expr)
 						if err != nil {
 							return nil, err
 						}
 
-						for name, value := range v.AsValueMap() {
+						for k, value := range v.AsValueMap() {
 							if value.Type() != cty.String {
 								return nil, fmt.Errorf("value in 'environment_variables' is not a string")
 							}
 
-							envVars[name] = value
+							envVars[k] = value
 						}
 					} else {
 						attrs[name] = attr // Currently not used
@@ -494,8 +494,8 @@ func mergeDefaults(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
 	}
 
 	if len(envVars) > 0 {
-		attrs["environment_variables"] = &hclsyntax.Attribute{
-			Name: "environment_variables",
+		attrs[environmentVars] = &hclsyntax.Attribute{
+			Name: environmentVars,
 			Expr: &hclsyntax.LiteralValueExpr{
 				Val: cty.MapVal(envVars),
 			},
diff --git a/config/configload/schema.go b/config/configload/schema.go
index 1a79fe29..8b0a7a11 100644
--- a/config/configload/schema.go
+++ b/config/configload/schema.go
@@ -153,7 +153,7 @@ func getSchemaComponents(body hcl.Body, obj interface{}) (hcl.Blocks, hcl.Diagno
 
 	// TODO: How to implement this automatically?
 	if typ.String() == "config.Backend" {
-		schema = meta.MergeSchemas(schema, config.OAuthBlockSchema)
+		meta.MergeSchemas(schema, config.OAuthBlockSchema, config.TokenRequestBlockSchema)
 	}
 
 	if _, ok := obj.(collect.ErrorHandlerSetter); ok {
diff --git a/config/configload/validate.go b/config/configload/validate.go
index c03ac6b3..c65d4087 100644
--- a/config/configload/validate.go
+++ b/config/configload/validate.go
@@ -49,7 +49,7 @@ func uniqueLabelName(unique map[string]struct{}, name string, hr *hcl.Range) err
 	return nil
 }
 
-func verifyBodyAttributes(content *hcl.BodyContent) error {
+func verifyBodyAttributes(blockName string, content *hcl.BodyContent) error {
 	_, existsBody := content.Attributes["body"]
 	_, existsFormBody := content.Attributes["form_body"]
 	_, existsJsonBody := content.Attributes["json_body"]
@@ -61,7 +61,7 @@ func verifyBodyAttributes(content *hcl.BodyContent) error {
 		}
 
 		return newDiagErr(&content.Attributes[rangeAttr].Range,
-			"request can only have one of body, form_body or json_body attributes")
+			blockName+" can only have one of body, form_body or json_body attributes")
 	}
 
 	return nil
diff --git a/config/generate/main.go b/config/generate/main.go
index 80fd644e..776254cb 100644
--- a/config/generate/main.go
+++ b/config/generate/main.go
@@ -76,6 +76,7 @@ func main() {
 		&config.Request{},
 		&config.Server{},
 		&config.Settings{},
+		&config.TokenRequest{},
 		&config.Websockets{},
 	} {
 		t := reflect.TypeOf(impl).Elem()
diff --git a/config/runtime/backend.go b/config/runtime/backend.go
index fe383e8a..ffe52ba4 100644
--- a/config/runtime/backend.go
+++ b/config/runtime/backend.go
@@ -13,6 +13,7 @@ import (
 	"github.com/avenga/couper/backend"
 	"github.com/avenga/couper/cache"
 	"github.com/avenga/couper/config"
+	hclbody "github.com/avenga/couper/config/body"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler/transport"
@@ -97,10 +98,21 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 	oauthContent, _, _ := backendCtx.PartialContent(config.OAuthBlockSchema)
 	if oauthContent != nil {
 		if blocks := oauthContent.Blocks.OfType("oauth2"); len(blocks) > 0 {
-			options.AuthBackend, err = newAuthBackend(evalCtx, beConf, blocks, log, conf, memStore)
+			requestAuthz, err := newOAuth2RequestAuthorizer(evalCtx, beConf, blocks, log, conf, memStore)
 			if err != nil {
 				return nil, err
 			}
+			options.RequestAuthz = append(options.RequestAuthz, requestAuthz)
+		}
+	}
+	tokenRequestContent, _, _ := backendCtx.PartialContent(config.TokenRequestBlockSchema)
+	if tokenRequestContent != nil {
+		for _, block := range tokenRequestContent.Blocks.OfType("beta_token_request") {
+			requestAuthz, err := newTokenRequestAuthorizer(evalCtx, beConf, block, log, conf, memStore)
+			if err != nil {
+				return nil, err
+			}
+			options.RequestAuthz = append(options.RequestAuthz, requestAuthz)
 		}
 	}
 
@@ -108,23 +120,23 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 	return b, nil
 }
 
-func newAuthBackend(evalCtx *hcl.EvalContext, beConf *config.Backend, blocks hcl.Blocks, log *logrus.Entry,
-	conf *config.Couper, memStore *cache.MemoryStore) (transport.TokenRequest, error) {
+func newOAuth2RequestAuthorizer(evalCtx *hcl.EvalContext, beConf *config.Backend, blocks hcl.Blocks, log *logrus.Entry,
+	conf *config.Couper, memStore *cache.MemoryStore) (transport.RequestAuthorizer, error) {
 
-	beConf.OAuth2 = &config.OAuth2ReqAuth{}
+	oaConfig := &config.OAuth2ReqAuth{}
 
-	if diags := gohcl.DecodeBody(blocks[0].Body, evalCtx, beConf.OAuth2); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(blocks[0].Body, evalCtx, oaConfig); diags.HasErrors() {
 		return nil, diags
 	}
 
-	innerContent, _, diags := beConf.OAuth2.Remain.PartialContent(beConf.OAuth2.Schema(true))
+	innerContent, _, diags := oaConfig.Remain.PartialContent(oaConfig.Schema(true))
 	if diags.HasErrors() {
 		return nil, diags
 	}
 
 	backendBlocks := innerContent.Blocks.OfType("backend")
 	if len(backendBlocks) == 0 {
-		r := beConf.OAuth2.Remain.MissingItemRange()
+		r := oaConfig.Remain.MissingItemRange()
 		diag := &hcl.Diagnostics{&hcl.Diagnostic{
 			Subject: &r,
 			Summary: "missing backend initialization",
@@ -138,12 +150,12 @@ func newAuthBackend(evalCtx *hcl.EvalContext, beConf *config.Backend, blocks hcl
 	}
 
 	// Set default value
-	if beConf.OAuth2.Retries == nil {
+	if oaConfig.Retries == nil {
 		var one uint8 = 1
-		beConf.OAuth2.Retries = &one
+		oaConfig.Retries = &one
 	}
 
-	tr, err := transport.NewOAuth2ReqAuth(beConf.OAuth2, memStore, authBackend)
+	tr, err := transport.NewOAuth2ReqAuth(oaConfig, memStore, authBackend)
 	if err != nil {
 		return nil, errors.Backend.Label(beConf.Name).With(err)
 	}
@@ -151,6 +163,47 @@ func newAuthBackend(evalCtx *hcl.EvalContext, beConf *config.Backend, blocks hcl
 	return tr, nil
 }
 
+func newTokenRequestAuthorizer(evalCtx *hcl.EvalContext, beConf *config.Backend, block *hcl.Block, log *logrus.Entry,
+	conf *config.Couper, memStore *cache.MemoryStore) (transport.RequestAuthorizer, error) {
+
+	label := "default"
+	if len(block.Labels) > 0 {
+		label = block.Labels[0]
+	}
+	trConfig := &config.TokenRequest{Name: label}
+
+	if diags := gohcl.DecodeBody(block.Body, evalCtx, trConfig); diags.HasErrors() {
+		return nil, diags
+	}
+
+	innerContent, _, diags := trConfig.Remain.PartialContent(trConfig.Schema(true))
+	if diags.HasErrors() {
+		return nil, diags
+	}
+
+	// TODO find better place to rename these attributes (during config load?)
+	hclbody.RenameAttribute(innerContent, "headers", "set_request_headers")
+	hclbody.RenameAttribute(innerContent, "query_params", "set_query_params")
+
+	backendBlocks := innerContent.Blocks.OfType("backend")
+	if len(backendBlocks) == 0 {
+		r := trConfig.Remain.MissingItemRange()
+		diag := &hcl.Diagnostics{&hcl.Diagnostic{
+			Subject: &r,
+			Summary: "missing backend initialization",
+		}}
+		return nil, errors.Configuration.Label("unexpected").With(diag)
+	}
+	innerBackend := backendBlocks[0] // backend block is set by configload
+	authBackend, authErr := NewBackend(evalCtx, innerBackend.Body, log, conf, memStore)
+	if authErr != nil {
+		return nil, authErr
+	}
+
+	tr, err := transport.NewTokenRequest(trConfig, memStore, authBackend, beConf.Reference())
+	return tr, err
+}
+
 func getBackendName(evalCtx *hcl.EvalContext, backendCtx hcl.Body) (string, error) {
 	content, _, _ := backendCtx.PartialContent(&hcl.BodySchema{Attributes: []hcl.AttributeSchema{
 		{Name: "name"}},
diff --git a/config/sequence/sequence.go b/config/sequence/sequence.go
index de7ad7cc..dee0e935 100644
--- a/config/sequence/sequence.go
+++ b/config/sequence/sequence.go
@@ -9,9 +9,17 @@ import (
 
 type List []*Item
 
+func NewBackendItem(name string) *Item {
+	return &Item{
+		Name:    name,
+		backend: true,
+	}
+}
+
 type Item struct {
 	BodyRange *hcl.Range
 	Name      string
+	backend   bool
 	deps      List
 	parent    *Item
 	seen      map[string]struct{}
@@ -24,7 +32,7 @@ func (s *Item) Add(ref *Item) *Item {
 
 	ref.parent = s
 
-	if s.hasSeen(ref.Name) { // collect names to populate error message
+	if s.backend && s.hasAncestor(ref.Name) || !s.backend && s.hasSeen(ref.Name) { // collect names to populate error message
 		refs := []string{ref.Name}
 		p := s.parent
 		for p != s {
@@ -72,6 +80,22 @@ func (s *Item) HasParent() bool {
 	return s != nil && s.parent != nil
 }
 
+func (s *Item) hasAncestor(name string) bool {
+	if s == nil {
+		return false
+	}
+
+	if !s.HasParent() {
+		return false
+	}
+
+	if s.parent.Name == name {
+		return true
+	}
+
+	return s.parent.hasAncestor(name)
+}
+
 func (s *Item) hasSeen(name string) bool {
 	if s == nil {
 		return false
diff --git a/config/token_request.go b/config/token_request.go
new file mode 100644
index 00000000..099e6d41
--- /dev/null
+++ b/config/token_request.go
@@ -0,0 +1,77 @@
+package config
+
+import (
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+	"github.com/zclconf/go-cty/cty"
+)
+
+var (
+	_ BackendReference = &TokenRequest{}
+	_ Inline           = &TokenRequest{}
+)
+
+var TokenRequestBlockSchema = &hcl.BodySchema{
+	Blocks: []hcl.BlockHeaderSchema{
+		{
+			Type:          "beta_token_request",
+			LabelNames:    []string{"name"},
+			LabelOptional: true,
+		},
+	},
+}
+
+type TokenRequest struct {
+	BackendName string   `hcl:"backend,optional" docs:"backend block reference is required if no backend block is defined"`
+	Name        string   `hcl:"name,label,optional"`
+	URL         string   `hcl:"url,optional" docs:"If defined, the host part of the URL must be the same as the <code>origin</code> attribute of the <code>backend</code> block (if defined)."`
+	Remain      hcl.Body `hcl:",remain"`
+
+	// Internally used
+	Backend hcl.Body
+}
+
+// Reference implements the <BackendReference> interface.
+func (t *TokenRequest) Reference() string {
+	return t.BackendName
+}
+
+// HCLBody implements the <Inline> interface.
+func (t *TokenRequest) HCLBody() hcl.Body {
+	return t.Remain
+}
+
+// Inline implements the <Inline> interface.
+func (t *TokenRequest) Inline() interface{} {
+	type Inline struct {
+		Backend        *Backend             `hcl:"backend,block"`
+		Body           string               `hcl:"body,optional" docs:"Creates implicit default <code>Content-Type: text/plain</code> header field"`
+		ExpectedStatus []int                `hcl:"expected_status,optional" docs:"If defined, the response status code will be verified against this list of status codes, If the status code is unexpected a <code>beta_backend_token_request</code> error can be handled with an <code>error_handler</code>"`
+		FormBody       string               `hcl:"form_body,optional" docs:"Creates implicit default <code>Content-Type: application/x-www-form-urlencoded</code> header field."`
+		Headers        map[string]string    `hcl:"headers,optional" docs:"sets the given request headers"`
+		JsonBody       string               `hcl:"json_body,optional" docs:"Creates implicit default <code>Content-Type: application/json</code> header field" type:"null, bool, number, string, object, tuple"`
+		Method         string               `hcl:"method,optional" default:"GET"`
+		QueryParams    map[string]cty.Value `hcl:"query_params,optional" docs:"sets the url query parameters"`
+		TTL            string               `hcl:"ttl" docs:"The time span for which the token is to be stores."`
+		Token          string               `hcl:"token" docs:"The token to be stored in <code>backends.<backend_name>.tokens.<token_request_name></code>"`
+	}
+
+	return &Inline{}
+}
+
+// Schema implements the <Inline> interface.
+func (t *TokenRequest) Schema(inline bool) *hcl.BodySchema {
+	if !inline {
+		schema, _ := gohcl.ImpliedBodySchema(t)
+		return schema
+	}
+
+	schema, _ := gohcl.ImpliedBodySchema(t.Inline())
+
+	// A backend reference is defined, backend block is not allowed.
+	if t.BackendName != "" {
+		schema.Blocks = nil
+	}
+
+	return schema
+}
diff --git a/docs/website/content/2.configuration/4.block/backend.md b/docs/website/content/2.configuration/4.block/backend.md
index fc425ebd..14df717b 100644
--- a/docs/website/content/2.configuration/4.block/backend.md
+++ b/docs/website/content/2.configuration/4.block/backend.md
@@ -10,9 +10,9 @@ The `backend` block defines the connection to a local/remote backend service.
 
 Backends can be defined in the [Definitions Block](definitions) and referenced by _label_.
 
-| Block name | Context                                                                                                                                                                                                                                   | Label                                                                     | Nested block(s)                                                                                  |
-|:-----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
-| `backend`  | [Definitions Block](definitions), [Proxy Block](proxy), [Request Block](request), [OAuth2 CC Block](oauth2req_auth), [JWT Block](jwt), [OAuth2 AC Block (beta)](oauth2), [OIDC Block](oidc) | &#9888; required, when defined in [Definitions Block](definitions) | [OpenAPI Block](openapi), [OAuth2 CC Block](oauth2req_auth), [Health Block](health) |
+| Block name | Context                                                                                                                                                                                                                                   | Label                                                                     | Nested block(s)                                                                                                                       |
+|:-----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
+| `backend`  | [Definitions Block](definitions), [Proxy Block](proxy), [Request Block](request), [OAuth2 CC Block](oauth2req_auth), [JWT Block](jwt), [OAuth2 AC Block (beta)](oauth2), [OIDC Block](oidc)                                               | &#9888; required, when defined in [Definitions Block](definitions)        | [OpenAPI Block](openapi), [OAuth2 CC Block](oauth2req_auth), [Health Block](health), [Token Request (Beta) Block](beta_token_request) |
 
 ::attributes
 ---
diff --git a/docs/website/content/2.configuration/4.block/token_request.md b/docs/website/content/2.configuration/4.block/token_request.md
new file mode 100644
index 00000000..516ea57b
--- /dev/null
+++ b/docs/website/content/2.configuration/4.block/token_request.md
@@ -0,0 +1,76 @@
+# Token Request (Beta)
+
+The `beta_token_request` block in the [Backend Block](backend) context configures a request to get a token used to authorize backend requests.
+
+| Block name            | Context                           | Label                                                                                                                                                                                                                       | Nested block(s)                                                                                                      |
+|:----------------------|:----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------|
+| `beta_token_request`  | [Backend Block](backend)          | &#9888; A [Token Request (Beta) Block](token_request) w/o a label has an implicit label `"default"`. Only **one** [Token Request (Beta) Block](token_request) w/ label `"default"` per [Backend Block](backend) is allowed. | [Backend Block](backend) (&#9888; required, if no `backend` block reference is defined or no `url` attribute is set. |
+<!-- TODO: add available http methods -->
+
+::attributes
+---
+values: [
+  {
+    "name": "backend",
+    "type": "string",
+    "default": "",
+    "description": "backend block reference is required if no backend block is defined"
+  },
+  {
+    "name": "url",
+    "type": "string",
+    "default": "",
+    "description": "If defined, the host part of the URL must be the same as the <code>origin</code> attribute of the <code>backend</code> block (if defined)."
+  },
+  {
+    "name": "body",
+    "type": "string",
+    "default": "",
+    "description": "Creates implicit default <code>Content-Type: text/plain</code> header field"
+  },
+  {
+    "name": "expected_status",
+    "type": "tuple (int)",
+    "default": "[]",
+    "description": "If defined, the response status code will be verified against this list of status codes, If the status code is unexpected a <code>beta_backend_token_request</code> error can be handled with an <code>error_handler</code>"
+  },
+  {
+    "name": "form_body",
+    "type": "string",
+    "default": "",
+    "description": "Creates implicit default <code>Content-Type: application/x-www-form-urlencoded</code> header field."
+  },
+  {
+    "name": "headers",
+    "type": "object",
+    "default": "",
+    "description": "sets the given request headers"
+  },
+  {
+    "name": "json_body",
+    "type": "null, bool, number, string, object, tuple",
+    "default": "",
+    "description": "Creates implicit default <code>Content-Type: application/json</code> header field"
+  },
+  {
+    "name": "query_params",
+    "type": "object",
+    "default": "",
+    "description": "sets the url query parameters"
+  },
+  {
+    "name": "token",
+    "type": "string",
+    "default": "",
+    "description": "The token to be stored in <code>backends.<backend_name>.tokens.<token_request_name></code>"
+  },
+  {
+    "name": "ttl",
+    "type": "string",
+    "default": "",
+    "description": "The time span for which the token is to be stores."
+  }
+]
+
+---
+::
diff --git a/docs/website/content/2.configuration/5.variables.md b/docs/website/content/2.configuration/5.variables.md
index 8dee3da0..10d34ad7 100644
--- a/docs/website/content/2.configuration/5.variables.md
+++ b/docs/website/content/2.configuration/5.variables.md
@@ -83,14 +83,23 @@ and for an [`oidc` block](block/oidc) additionally:
 - `id_token_claims`: a map of claims from the ID token
 - `userinfo`: a map of claims retrieved from the userinfo endpoint
 
+## `beta_token_response`
+
+Only available in the [`beta_token_request` block](block/token_request) context, `beta_token_response` allows access to the current token response (see [`backend_responses`](#backend_responses) for available properties).
 
 ## `backends`
 
 `backends.<label>` allows access to backend information.
 
-| Variable | Type   | Description                           | Example                                              |
-|:---------|:-------|:--------------------------------------|:-----------------------------------------------------|
-| `health` | object | current [health state](block/health) | `{"error": "", "healthy": true, "state": "healthy"}` |
+| Variable                           | Type   | Description                                                                                         | Example                                              |
+|:-----------------------------------|:-------|:----------------------------------------------------------------------------------------------------|:-----------------------------------------------------|
+| `health`                           | object | current [health state](block/health)                                                                | `{"error": "", "healthy": true, "state": "healthy"}` |
+| `beta_tokens.<token_request_name>` | string | The token obtained by the [token request](block/token_request) with name `<token_request_name>`     |                                                      |
+| `beta_token`                       | string | The token obtained by the [token request](block/token_request) with name `"default"`, if configured |                                                      |
+
+## `beta_backend`
+
+Only available in the `backend` block context, `beta_backend` allows access to the current backend information (see [`backends`](#backends) for available properties).
 
 ## `backend_request`
 
diff --git a/docs/website/content/2.configuration/7.error-handling.md b/docs/website/content/2.configuration/7.error-handling.md
index f4792e0b..f412dfc9 100644
--- a/docs/website/content/2.configuration/7.error-handling.md
+++ b/docs/website/content/2.configuration/7.error-handling.md
@@ -54,6 +54,7 @@ The following table documents error types that can be handled in `api` blocks:
 | `backend_openapi_validation` (`backend`)           | Backend request or response is invalid                                                                  | Send error template with status code `400` for invalid backend request or `502` for invalid backend response. |
 | `backend_timeout` (`backend`)                      | A backend request timed out                                                                             | Send error template with status `504`.                                                                        |
 | `backend_unhealthy` (`backend`)                    | A backend is unhealthy and will not send the request                                                    | Send error template with status `502`.                                                                        |
+| `beta_backend_token_request` (`backend`)           | A token request for the backend has failed                                                              | Send error template with status `502`.                                                                        |
 | `access_control`                                   | Access control related errors                                                                           | Send error template with status `403`.                                                                        |
 | `beta_insufficient_permissions` (`access_control`) | The permission required for the requested operation is not in the permissions granted to the requester. | Send error template with status `403`.                                                                        |
 
@@ -67,6 +68,7 @@ The following table documents error types that can be handled in `endpoint` bloc
 | `backend_openapi_validation` (`backend`)           | Backend request or response is invalid                                                                  | Send error template with status code `400` for invalid backend request or `502` for invalid backend response. |
 | `backend_timeout` (`backend`)                      | A backend request timed out                                                                             | Send error template with status `504`.                                                                        |
 | `backend_unhealthy` (`backend`)                    | A backend is unhealthy and will not send the request                                                    | Send error template with status `502`.                                                                        |
+| `beta_backend_token_request` (`backend`)           | A token request for the backend has failed                                                              | Send error template with status `502`.                                                                        |
 | `access_control`                                   | Access control related errors                                                                           | Send error template with status `403`.                                                                        |
 | `beta_insufficient_permissions` (`access_control`) | The permission required for the requested operation is not in the permissions granted to the requester. | Send error template with status `403`.                                                                        |
 | `endpoint`                                         | All catchable `endpoint` related errors                                                                 | Send error template with status `502`.                                                                        |
diff --git a/errors/type_defintions.go b/errors/type_defintions.go
index e14f80c0..556365a8 100644
--- a/errors/type_defintions.go
+++ b/errors/type_defintions.go
@@ -25,6 +25,7 @@ var Definitions = []*Error{
 	Backend,
 	Backend.Kind("backend_openapi_validation").Status(http.StatusBadRequest),
 	Backend.Kind("backend_timeout").Status(http.StatusGatewayTimeout),
+	Backend.Kind("beta_backend_token_request"),
 	Backend.Kind("backend_unhealthy"),
 
 	Endpoint,
diff --git a/errors/types_generated.go b/errors/types_generated.go
index ebd89810..b4f29cdc 100644
--- a/errors/types_generated.go
+++ b/errors/types_generated.go
@@ -15,9 +15,10 @@ var (
 	BetaInsufficientPermissions = Definitions[10]
 	BackendOpenapiValidation    = Definitions[12]
 	BackendTimeout              = Definitions[13]
-	BackendUnhealthy            = Definitions[14]
-	Sequence                    = Definitions[16]
-	UnexpectedStatus            = Definitions[17]
+	BetaBackendTokenRequest     = Definitions[14]
+	BackendUnhealthy            = Definitions[15]
+	Sequence                    = Definitions[17]
+	UnexpectedStatus            = Definitions[18]
 )
 
 // typeDefinitions holds all related error definitions which are
@@ -41,6 +42,7 @@ var types = typeDefinitions{
 	"backend":                        Backend,
 	"backend_openapi_validation":     BackendOpenapiValidation,
 	"backend_timeout":                BackendTimeout,
+	"beta_backend_token_request":     BetaBackendTokenRequest,
 	"backend_unhealthy":              BackendUnhealthy,
 	"endpoint":                       Endpoint,
 	"sequence":                       Sequence,
@@ -56,4 +58,4 @@ func IsKnown(errorType string) bool {
 
 // SuperTypesMapsByContext holds maps for error super-types to sub-types
 // by a given context block type (e.g. api or endpoint).
-var SuperTypesMapsByContext = map[string]map[string][]string{"api": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "backend_unhealthy"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "backend_unhealthy"}}, "endpoint": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "backend_unhealthy", "sequence", "unexpected_status"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "backend_unhealthy"}, "endpoint": []string{"sequence", "unexpected_status"}}}
+var SuperTypesMapsByContext = map[string]map[string][]string{"api": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}}, "endpoint": map[string][]string{"*": []string{"beta_insufficient_permissions", "backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy", "sequence", "unexpected_status"}, "access_control": []string{"beta_insufficient_permissions"}, "backend": []string{"backend_openapi_validation", "backend_timeout", "beta_backend_token_request", "backend_unhealthy"}, "endpoint": []string{"sequence", "unexpected_status"}}}
diff --git a/eval/context.go b/eval/context.go
index d95df282..bae39b9f 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -187,18 +187,7 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
 	ctx.eval.Variables[BackendRequests] = cty.ObjectVal(make(map[string]cty.Value))
 	ctx.eval.Variables[BackendResponses] = cty.ObjectVal(make(map[string]cty.Value))
 
-	backendsVariable := map[string]cty.Value{}
-	for _, backend := range ctx.backends {
-		b, ok := backend.(seetie.Object)
-		if !ok {
-			continue
-		}
-		v := b.Value()
-		vm := v.AsValueMap()
-		backendsVariable[vm["name"].AsString()] = v
-	}
-	ctx.eval.Variables[Backends] = cty.ObjectVal(backendsVariable)
-
+	mergeBackendVariables(ctx.eval, Backends, ctx.syncBackendVariables())
 	ctx.updateRequestRelatedFunctions(origin)
 	ctx.updateFunctions()
 
@@ -233,8 +222,9 @@ func (c *Context) WithBeresp(beresp *http.Response, readBody bool) *Context {
 
 	// Prevent overriding existing variables with successive calls to this method.
 	// Could happen with error_handler within an endpoint. Merge them.
-	c.updateBackendVariables(ctx.eval, BackendRequests, bereqs)
-	c.updateBackendVariables(ctx.eval, BackendResponses, resps)
+	mergeBackendVariables(ctx.eval, Backends, ctx.syncBackendVariables())
+	mergeBackendVariables(ctx.eval, BackendRequests, bereqs)
+	mergeBackendVariables(ctx.eval, BackendResponses, resps)
 
 	clientOrigin, _ := seetie.ValueToMap(ctx.eval.Variables[ClientRequest])[Origin].(string)
 	originUrl, _ := url.Parse(clientOrigin)
@@ -304,15 +294,18 @@ func newBerespValues(ctx context.Context, readBody bool, beresp *http.Response)
 	return name, bereqVal, berespVal
 }
 
-func (c *Context) updateBackendVariables(evalCtx *hcl.EvalContext, key string, cmap ContextMap) {
-	if !evalCtx.Variables[key].IsNull() && evalCtx.Variables[key].LengthInt() > 0 {
-		merged, _ := lib.Merge([]cty.Value{evalCtx.Variables[key], cty.ObjectVal(cmap)})
-		if !merged.IsNull() {
-			evalCtx.Variables[key] = merged
+func (c *Context) syncBackendVariables() map[string]cty.Value {
+	backendsVariable := make(map[string]cty.Value)
+	for _, backend := range c.backends {
+		b, ok := backend.(seetie.Object)
+		if !ok {
+			continue
 		}
-	} else {
-		evalCtx.Variables[key] = cty.ObjectVal(cmap)
+		v := b.Value()
+		vm := v.AsValueMap()
+		backendsVariable[vm["name"].AsString()] = v
 	}
+	return backendsVariable
 }
 
 // WithJWTSigningConfigs initially sets up the lib.FnJWTSign function.
@@ -373,6 +366,9 @@ func (c *Context) HCLContextSync() *hcl.EvalContext {
 	e := c.cloneEvalContext()
 	c.syncedVariables.Sync(e.Variables)
 
+	backendsValue := c.syncBackendVariables()
+	mergeBackendVariables(e, Backends, backendsValue)
+
 	return e
 }
 
@@ -431,6 +427,17 @@ func (c *Context) cloneEvalContext() *hcl.EvalContext {
 	return ctx
 }
 
+func mergeBackendVariables(etx *hcl.EvalContext, key string, cmap ContextMap) {
+	if !etx.Variables[key].IsNull() && etx.Variables[key].LengthInt() > 0 {
+		merged, _ := lib.Merge([]cty.Value{etx.Variables[key], cty.ObjectVal(cmap)})
+		if !merged.IsNull() {
+			etx.Variables[key] = merged
+		}
+	} else {
+		etx.Variables[key] = cty.ObjectVal(cmap)
+	}
+}
+
 const defaultMaxMemory = 32 << 20 // 32 MB
 
 // parseForm populates the request PostForm field.
@@ -615,6 +622,17 @@ func newCtyCouperVariablesMap(environment string) cty.Value {
 	return cty.MapVal(ctyMap)
 }
 
+func MapTokenResponse(evalCtx *hcl.EvalContext, name string) {
+	if name == "" {
+		name = "default"
+	}
+
+	responses := evalCtx.Variables[BackendResponses].AsValueMap()
+	respValue, _ := responses[TokenRequestPrefix+name]
+
+	evalCtx.Variables[TokenResponse] = respValue
+}
+
 // Functions
 func newFunctionsMap() map[string]function.Function {
 	return map[string]function.Function{
diff --git a/eval/sync.go b/eval/sync.go
index c4c25014..cb39faef 100644
--- a/eval/sync.go
+++ b/eval/sync.go
@@ -23,8 +23,8 @@ func NewSyncedVariables() *SyncedVariables {
 
 type syncPair struct {
 	name          string
+	backendName   string
 	bereq, beresp cty.Value
-	tokenRequest  bool
 }
 
 // Set finalized cty req/resp pair.
@@ -33,13 +33,16 @@ func (sv *SyncedVariables) Set(beresp *http.Response) {
 	name, bereqV, berespV := newBerespValues(ctx, true, beresp)
 
 	if tr, ok := ctx.Value(request.TokenRequest).(string); ok && tr != "" {
-		name = TokenRequestPrefix + name
+		name = TokenRequestPrefix + tr
 	}
 
+	backendName, _ := ctx.Value(request.BackendName).(string)
+
 	sv.items.Store(name, &syncPair{
-		name:   name,
-		bereq:  bereqV,
-		beresp: berespV,
+		backendName: backendName,
+		bereq:       bereqV,
+		beresp:      berespV,
+		name:        name,
 	})
 }
 
@@ -48,6 +51,7 @@ func (sv *SyncedVariables) Sync(variables map[string]cty.Value) {
 	if brs, ok := variables[BackendRequests]; ok {
 		bereqs = brs.AsValueMap()
 	}
+
 	if brps, ok := variables[BackendResponses]; ok {
 		beresps = brps.AsValueMap()
 	}
diff --git a/eval/variables.go b/eval/variables.go
index a13ba0e6..44ec70e0 100644
--- a/eval/variables.go
+++ b/eval/variables.go
@@ -1,13 +1,12 @@
 package eval
 
 const (
+	Backend          = "beta_backend"
 	Backends         = "backends"
 	BackendRequest   = "backend_request"
 	BackendRequests  = "backend_requests"
 	BackendResponse  = "backend_response"
 	BackendResponses = "backend_responses"
-	BackendDefault   = "default"
-	State            = "state"
 	Body             = "body"
 	ClientRequest    = "request"
 	CTX              = "context"
@@ -16,7 +15,6 @@ const (
 	Environment      = "env"
 	FormBody         = "form_body"
 	Headers          = "headers"
-	Health           = "health"
 	HttpStatus       = "status"
 	ID               = "id"
 	JsonBody         = "json_body"
@@ -24,6 +22,7 @@ const (
 	Path             = "path"
 	PathParam        = "path_params"
 	Query            = "query"
+	TokenResponse    = "beta_token_response"
 	URL              = "url"
 	Origin           = "origin"
 	Protocol         = "protocol"
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index 77211d95..7c6f79b8 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -50,7 +50,7 @@ type Backend struct {
 	logEntry            *logrus.Entry
 	name                string
 	openAPIValidator    *validation.OpenAPI
-	tokenRequest        TokenRequest
+	requestAuthorizer   []RequestAuthorizer
 	transport           *http.Transport
 	transportConf       *Config
 	transportConfResult Config
@@ -61,25 +61,25 @@ type Backend struct {
 // NewBackend creates a new <*Backend> object by the given <*Config>.
 func NewBackend(ctx hcl.Body, tc *Config, opts *BackendOptions, log *logrus.Entry) http.RoundTripper {
 	var (
-		healthCheck  *config.HealthCheck
-		openAPI      *validation.OpenAPI
-		tokenRequest TokenRequest
+		healthCheck       *config.HealthCheck
+		openAPI           *validation.OpenAPI
+		requestAuthorizer []RequestAuthorizer
 	)
 
 	if opts != nil {
 		healthCheck = opts.HealthCheck
 		openAPI = validation.NewOpenAPI(opts.OpenAPI)
-		tokenRequest = opts.AuthBackend
+		requestAuthorizer = opts.RequestAuthz
 	}
 
 	backend := &Backend{
-		context:          ctx,
-		healthInfo:       &HealthInfo{Healthy: true, State: StateOk.String()},
-		logEntry:         log.WithField("backend", tc.BackendName),
-		name:             tc.BackendName,
-		openAPIValidator: openAPI,
-		tokenRequest:     tokenRequest,
-		transportConf:    tc,
+		context:           ctx,
+		healthInfo:        &HealthInfo{Healthy: true, State: StateOk.String()},
+		logEntry:          log.WithField("backend", tc.BackendName),
+		name:              tc.BackendName,
+		openAPIValidator:  openAPI,
+		requestAuthorizer: requestAuthorizer,
+		transportConf:     tc,
 	}
 
 	backend.upstreamLog = logging.NewUpstreamLog(backend.logEntry, backend, tc.NoProxyFromEnv)
@@ -114,20 +114,25 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		ctxBody = hclbody.MergeBodies(b.context, ctxBody)
 	}
 
+	// originalReq for token-request retry purposes
+	originalReq, err := b.withTokenRequest(req)
+	if err != nil {
+		return nil, errors.BetaBackendTokenRequest.Label(b.name).With(err)
+	}
+
 	hclCtx := eval.ContextFromRequest(req).HCLContextSync()
+	if v, ok := hclCtx.Variables[eval.Backends]; ok {
+		if m, exist := v.AsValueMap()[b.name]; exist {
+			hclCtx.Variables[eval.Backend] = m
+		}
+	}
 
-	if err := b.isUnhealthy(hclCtx, ctxBody); err != nil {
+	if err = b.isUnhealthy(hclCtx, ctxBody); err != nil {
 		return &http.Response{
 			Request: req, // provide outreq (variable) on error cases
 		}, err
 	}
 
-	// originalReq for token-request retry purposes
-	originalReq, err := b.withTokenRequest(req)
-	if err != nil {
-		return nil, err
-	}
-
 	logCh, _ := req.Context().Value(request.LogCustomUpstream).(chan hcl.Body)
 	if logCh != nil {
 		logCh <- ctxBody
@@ -182,8 +187,8 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		}
 	}
 
-	b.withBasicAuth(req, ctxBody)
-	if err = b.withPathPrefix(req, ctxBody); err != nil {
+	b.withBasicAuth(req, hclCtx, ctxBody)
+	if err = b.withPathPrefix(req, hclCtx, ctxBody); err != nil {
 		return nil, err
 	}
 
@@ -214,8 +219,10 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		return beresp, err
 	}
 
+	req = req.WithContext(context.WithValue(req.Context(), request.BackendName, b.name))
+
 	if retry, rerr := b.withRetryTokenRequest(req, beresp); rerr != nil {
-		return beresp, rerr
+		return beresp, errors.BetaBackendTokenRequest.Label(b.name).With(rerr)
 	} else if retry {
 		return b.RoundTrip(originalReq)
 	}
@@ -239,6 +246,7 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 	// has own body variable reference?
 	readBody := eval.MustBuffer(b.context)&eval.BufferResponse == eval.BufferResponse
 	evalCtx = evalCtx.WithBeresp(beresp, readBody)
+
 	err = eval.ApplyResponseContext(evalCtx.HCLContext(), ctxBody, beresp)
 
 	if varSync, ok := req.Context().Value(request.ContextVariablesSynced).(*eval.SyncedVariables); ok {
@@ -318,41 +326,79 @@ func (b *Backend) innerRoundTrip(req *http.Request, tc *Config, deadlineErr <-ch
 }
 
 func (b *Backend) withTokenRequest(req *http.Request) (*http.Request, error) {
-	if b.tokenRequest == nil {
+	if b.requestAuthorizer == nil {
 		return nil, nil
 	}
 
 	trValue, _ := req.Context().Value(request.BackendTokenRequest).(string)
 	if trValue != "" { // prevent loop
-		return nil, nil
+		// TODO this prevents an oauth2 to send token request to (oauth2 or beta_token_request) authorized backend
+		// return nil, nil
 	}
 
 	ctx := context.WithValue(req.Context(), request.BackendTokenRequest, "tr")
 	// Reset for upstream transport; prevent mixing values.
-	// tokenRequest will have their own backend configuration.
+	// requestAuthorizer will have their own backend configuration.
 	ctx = context.WithValue(ctx, request.BackendParams, nil)
 
+	var cancel context.CancelFunc
+	ctx, cancel = context.WithCancel(ctx)
+	defer cancel()
+
 	originalReq := req.Clone(req.Context())
 
 	// WithContext() instead of Clone() due to header-map modification.
-	return originalReq, b.tokenRequest.WithToken(req.WithContext(ctx))
+	req = req.WithContext(ctx)
+
+	errorsCh := make(chan error, len(b.requestAuthorizer))
+	for _, authorizer := range b.requestAuthorizer {
+		err := authorizer.GetToken(req)
+		if err != nil {
+			return originalReq, err
+		}
+
+		go func(ra RequestAuthorizer, r *http.Request) {
+			errorsCh <- ra.GetToken(r)
+		}(authorizer, req)
+	}
+
+	var err error
+	for i := 0; i < len(b.requestAuthorizer); i++ {
+		err = <-errorsCh
+		if err != nil {
+			break
+		}
+	}
+	return originalReq, err
 }
 
 func (b *Backend) withRetryTokenRequest(req *http.Request, res *http.Response) (bool, error) {
-	if b.tokenRequest == nil {
+	if len(b.requestAuthorizer) == 0 {
 		return false, nil
 	}
 
 	trValue, _ := req.Context().Value(request.BackendTokenRequest).(string)
 	if trValue != "" { // prevent loop
-		return false, nil
+		// TODO this prevents an oauth2 to send token request to (oauth2 or beta_token_request) authorized backend
+		// return false, nil
 	}
 
-	return b.tokenRequest.RetryWithToken(req, res)
+	var retry bool
+	for _, ra := range b.requestAuthorizer {
+		r, err := ra.RetryWithToken(req, res)
+		if err != nil {
+			return false, err
+		}
+		if r {
+			retry = true
+			break
+		}
+	}
+	return retry, nil
 }
 
-func (b *Backend) withPathPrefix(req *http.Request, hclContext hcl.Body) error {
-	if pathPrefix := b.getAttribute(req, "path_prefix", hclContext); pathPrefix != "" {
+func (b *Backend) withPathPrefix(req *http.Request, evalCtx *hcl.EvalContext, hclContext hcl.Body) error {
+	if pathPrefix := b.getAttribute(evalCtx, "path_prefix", hclContext); pathPrefix != "" {
 		// TODO: Check for a valid absolute path
 		if i := strings.Index(pathPrefix, "#"); i >= 0 {
 			return errors.Configuration.Messagef("path_prefix attribute: invalid fragment found in %q", pathPrefix)
@@ -366,15 +412,15 @@ func (b *Backend) withPathPrefix(req *http.Request, hclContext hcl.Body) error {
 	return nil
 }
 
-func (b *Backend) withBasicAuth(req *http.Request, hclContext hcl.Body) {
-	if creds := b.getAttribute(req, "basic_auth", hclContext); creds != "" {
+func (b *Backend) withBasicAuth(req *http.Request, evalCtx *hcl.EvalContext, hclContext hcl.Body) {
+	if creds := b.getAttribute(evalCtx, "basic_auth", hclContext); creds != "" {
 		auth := base64.StdEncoding.EncodeToString([]byte(creds))
 		req.Header.Set("Authorization", "Basic "+auth)
 	}
 }
 
-func (b *Backend) getAttribute(req *http.Request, name string, hclContext hcl.Body) string {
-	attrVal, err := eval.ValueFromBodyAttribute(eval.ContextFromRequest(req).HCLContext(), hclContext, name)
+func (b *Backend) getAttribute(evalContext *hcl.EvalContext, name string, hclContext hcl.Body) string {
+	attrVal, err := eval.ValueFromBodyAttribute(evalContext, hclContext, name)
 	if err != nil {
 		b.upstreamLog.LogEntry().WithError(errors.Evaluation.Label(b.name).With(err))
 	}
@@ -557,7 +603,17 @@ func (b *Backend) Value() cty.Value {
 	b.healthyMu.RLock()
 	defer b.healthyMu.RUnlock()
 
-	return seetie.GoToValue(map[string]interface{}{
+	var tokens map[string]interface{}
+	for _, auth := range b.requestAuthorizer {
+		if name, v := auth.value(); v != "" {
+			if tokens == nil {
+				tokens = make(map[string]interface{})
+			}
+			tokens[name] = v
+		}
+	}
+
+	result := map[string]interface{}{
 		"health": map[string]interface{}{
 			"healthy": b.healthInfo.Healthy,
 			"error":   b.healthInfo.Error,
@@ -569,7 +625,16 @@ func (b *Backend) Value() cty.Value {
 		"connect_timeout": b.transportConfResult.ConnectTimeout.String(),
 		"ttfb_timeout":    b.transportConfResult.TTFBTimeout.String(),
 		"timeout":         b.transportConfResult.Timeout.String(),
-	})
+	}
+
+	if tokens != nil {
+		result["beta_tokens"] = tokens
+		if token, ok := tokens["default"]; ok {
+			result["beta_token"] = token
+		}
+	}
+
+	return seetie.GoToValue(result)
 }
 
 // setUserAgent sets an empty one if none is present or empty
diff --git a/handler/transport/backend_options.go b/handler/transport/backend_options.go
index 654a1533..d8ed5725 100644
--- a/handler/transport/backend_options.go
+++ b/handler/transport/backend_options.go
@@ -9,12 +9,14 @@ import (
 
 // BackendOptions represents the transport <BackendOptions> object.
 type BackendOptions struct {
-	AuthBackend TokenRequest
-	HealthCheck *config.HealthCheck
-	OpenAPI     *validation.OpenAPIOptions
+	RequestAuthz []RequestAuthorizer
+	HealthCheck  *config.HealthCheck
+	OpenAPI      *validation.OpenAPIOptions
 }
 
-type TokenRequest interface {
-	WithToken(req *http.Request) error
+type RequestAuthorizer interface {
+	GetToken(req *http.Request) error
 	RetryWithToken(req *http.Request, res *http.Response) (bool, error)
+
+	value() (string, string)
 }
diff --git a/handler/transport/oauth2_req_auth.go b/handler/transport/oauth2_req_auth.go
index 927e2ced..5516ff1c 100644
--- a/handler/transport/oauth2_req_auth.go
+++ b/handler/transport/oauth2_req_auth.go
@@ -25,7 +25,7 @@ type OAuth2ReqAuth struct {
 // NewOAuth2ReqAuth implements the http.RoundTripper interface to wrap an existing Backend / http.RoundTripper
 // to retrieve a valid token before passing the initial out request.
 func NewOAuth2ReqAuth(conf *config.OAuth2ReqAuth, memStore *cache.MemoryStore,
-	asBackend http.RoundTripper) (TokenRequest, error) {
+	asBackend http.RoundTripper) (RequestAuthorizer, error) {
 
 	if conf.GrantType != "client_credentials" && conf.GrantType != "password" {
 		return nil, fmt.Errorf("grant_type %s not supported", conf.GrantType)
@@ -65,7 +65,7 @@ func NewOAuth2ReqAuth(conf *config.OAuth2ReqAuth, memStore *cache.MemoryStore,
 	return reqAuth, nil
 }
 
-func (oa *OAuth2ReqAuth) WithToken(req *http.Request) error {
+func (oa *OAuth2ReqAuth) GetToken(req *http.Request) error {
 	token := oa.readAccessToken()
 	if token != "" {
 		req.Header.Set("Authorization", "Bearer "+token)
@@ -117,7 +117,7 @@ func (oa *OAuth2ReqAuth) RetryWithToken(req *http.Request, res *http.Response) (
 	if retries, ok := ctx.Value(request.TokenRequestRetries).(*uint8); !ok || *retries < *oa.config.Retries {
 		*retries++ // increase ptr value instead of context value
 		req.Header.Del("Authorization")
-		err := oa.WithToken(req.WithContext(ctx)) // WithContext due to header manipulation
+		err := oa.GetToken(req.WithContext(ctx)) // WithContext due to header manipulation
 		return true, err
 	}
 	return false, nil
@@ -141,3 +141,8 @@ func (oa *OAuth2ReqAuth) updateAccessToken(token string, jData map[string]interf
 		oa.memStore.Set(oa.storageKey, token, ttl)
 	}
 }
+
+func (oa *OAuth2ReqAuth) value() (string, string) {
+	token := oa.readAccessToken()
+	return "oauth2", token
+}
diff --git a/handler/transport/token_request.go b/handler/transport/token_request.go
new file mode 100644
index 00000000..5749b574
--- /dev/null
+++ b/handler/transport/token_request.go
@@ -0,0 +1,197 @@
+package transport
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/avenga/couper/cache"
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/internal/seetie"
+)
+
+type TokenRequest struct {
+	authorizedBackendName string
+	backend               http.RoundTripper
+	config                *config.TokenRequest
+	getMu                 sync.Mutex
+	memStore              *cache.MemoryStore
+	storageKey            string
+}
+
+func NewTokenRequest(conf *config.TokenRequest, memStore *cache.MemoryStore, backend http.RoundTripper, authorizedBackendName string) (RequestAuthorizer, error) {
+	tr := &TokenRequest{
+		authorizedBackendName: authorizedBackendName,
+		backend:               backend,
+		config:                conf,
+		memStore:              memStore,
+	}
+	tr.storageKey = fmt.Sprintf("TokenRequest-%p", tr)
+	return tr, nil
+}
+
+func (t *TokenRequest) GetToken(req *http.Request) error {
+	if token, err := t.readToken(); err != nil {
+		return errors.Backend.Label(t.config.BackendName).Message("token read error").With(err)
+	} else if token != "" {
+		return nil
+	}
+
+	// block during read/request process
+	t.getMu.Lock()
+	defer t.getMu.Unlock()
+
+	token, terr := t.readToken()
+	if terr != nil {
+		return errors.Backend.Label(t.config.BackendName).Message("token read error").With(terr)
+	} else if token != "" {
+		return nil
+	}
+
+	ctx := eval.ContextFromRequest(req)
+	token, ttl, err := t.requestToken(ctx)
+	if err != nil {
+		return errors.Backend.Label(t.config.BackendName).Message("token request error").With(err)
+	}
+
+	t.memStore.Set(t.storageKey, token, ttl)
+	return nil
+}
+
+func (t *TokenRequest) RetryWithToken(_ *http.Request, _ *http.Response) (bool, error) {
+	return false, nil
+}
+
+func (t *TokenRequest) readToken() (string, error) {
+	if data := t.memStore.Get(t.storageKey); data != nil {
+		return data.(string), nil
+	}
+
+	return "", nil
+}
+
+func (t *TokenRequest) requestToken(etx *eval.Context) (string, int64, error) {
+	hclCtx := etx.HCLContextSync()
+
+	trConf := &config.TokenRequest{Remain: t.config.Remain}
+	bodyContent, _, diags := t.config.Remain.PartialContent(trConf.Schema(true))
+	if diags.HasErrors() {
+		return "", 0, diags
+	}
+
+	methodVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "method")
+	if err != nil {
+		return "", 0, err
+	}
+	method := seetie.ValueToString(methodVal)
+
+	urlVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "url")
+	if err != nil {
+		return "", 0, err
+	}
+	url := seetie.ValueToString(urlVal)
+
+	body, defaultContentType, err := eval.GetBody(hclCtx, bodyContent)
+	if err != nil {
+		return "", 0, err
+	}
+
+	if method == "" {
+		method = http.MethodGet
+
+		if len(body) > 0 {
+			method = http.MethodPost
+		}
+	}
+
+	outreq, err := http.NewRequest(strings.ToUpper(method), url, bytes.NewBufferString(body))
+	if err != nil {
+		return "", 0, err
+	}
+
+	if defaultContentType != "" {
+		outreq.Header.Set("Content-Type", defaultContentType)
+	}
+
+	err = eval.ApplyRequestContext(hclCtx, t.config.Remain, outreq)
+	if err != nil {
+		return "", 0, err
+	}
+
+	// outCtx with "client" context due to syncedVars, cancel etc.
+	outCtx := context.WithValue(etx, request.BufferOptions, eval.BufferResponse)
+	outCtx = context.WithValue(outCtx, request.TokenRequest, t.config.Name)
+
+	outreq = outreq.WithContext(outCtx)
+	resp, err := t.backend.RoundTrip(outreq)
+	if err != nil {
+		return "", 0, err
+	}
+
+	expectedStatusVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "expected_status")
+	if err != nil {
+		return "", 0, err
+	}
+	expStatus := seetie.ValueToIntSlice(expectedStatusVal)
+	if len(expStatus) > 0 {
+		var seen bool
+		for _, exp := range expStatus {
+			if resp.StatusCode == int(exp) {
+				seen = true
+				break
+			}
+		}
+		if !seen {
+			return "", 0, errors.UnexpectedStatus.Message("unexpected status")
+		}
+	}
+
+	// obtain synced and already read beresp value; map to context variables
+	hclCtx = etx.HCLContextSync()
+	eval.MapTokenResponse(hclCtx, t.config.Name)
+
+	tokenVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "token")
+	if err != nil {
+		return "", 0, err
+	}
+	if tokenVal.IsNull() {
+		return "", 0, errors.Backend.Label(t.config.BackendName).Message("token expression evaluates to null")
+	}
+	if tokenVal.Type() != cty.String {
+		return "", 0, errors.Backend.Label(t.config.BackendName).Message("token expression must evaluate to a string")
+	}
+
+	ttlVal, err := eval.ValueFromAttribute(hclCtx, bodyContent, "ttl")
+	if err != nil {
+		return "", 0, err
+	}
+	if ttlVal.IsNull() {
+		return "", 0, errors.Backend.Label(t.config.BackendName).Message("ttl expression evaluates to null")
+	}
+	if ttlVal.Type() != cty.String {
+		return "", 0, errors.Backend.Label(t.config.BackendName).Message("ttl expression must evaluate to a string")
+	}
+
+	token := tokenVal.AsString()
+	ttl := ttlVal.AsString()
+	dur, parseErr := time.ParseDuration(ttl)
+	if parseErr != nil {
+		return "", 0, errors.Backend.Label(t.config.BackendName).Message("parsing ttl").With(parseErr)
+	}
+
+	return token, int64(dur.Seconds()), nil
+}
+
+func (t *TokenRequest) value() (string, string) {
+	token, _ := t.readToken()
+	return t.config.Name, token
+}
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index 0b648af6..a1ee5a59 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -32,7 +32,8 @@ func ValueToMap(val cty.Value) map[string]interface{} {
 			result[k] = nil
 			continue
 		}
-		switch v.Type() {
+		t := v.Type()
+		switch t {
 		case cty.Bool:
 			result[k] = v.True()
 		case cty.String:
@@ -45,6 +46,10 @@ func ValueToMap(val cty.Value) map[string]interface{} {
 		case cty.Map(cty.NilType):
 			result[k] = nil
 		default:
+			if t.IsObjectType() {
+				result[k] = ValueToMap(v)
+				continue
+			}
 			if isTuple(v) {
 				result[k] = ValueToStringSlice(v)
 				continue
diff --git a/logging/access_log.go b/logging/access_log.go
index f89dd84f..83f6e3ff 100644
--- a/logging/access_log.go
+++ b/logging/access_log.go
@@ -45,12 +45,9 @@ func (log *AccessLog) Do(writer http.ResponseWriter, req *http.Request) {
 	startTime := req.Context().Value(request.StartTime).(time.Time)
 	fields := Fields{}
 
-	backendName, _ := req.Context().Value(request.BackendName).(string)
-	if backendName == "" {
-		endpointName, _ := req.Context().Value(request.Endpoint).(string)
-		if endpointName != "" {
-			fields["endpoint"] = endpointName
-		}
+	endpointName, _ := req.Context().Value(request.Endpoint).(string)
+	if endpointName != "" {
+		fields["endpoint"] = endpointName
 	}
 
 	fields["method"] = req.Method
diff --git a/logging/hooks/custom_logs.go b/logging/hooks/custom_logs.go
index 40241a99..3a55e50f 100644
--- a/logging/hooks/custom_logs.go
+++ b/logging/hooks/custom_logs.go
@@ -113,7 +113,7 @@ func syncedUpstreamContext(evalCtx *eval.Context, entry *logrus.Entry) *hcl.Eval
 
 	if _, ok := ctx.Variables[eval.BackendRequests]; ok {
 		for k, v := range ctx.Variables[eval.BackendRequests].AsValueMap() {
-			if isTr && k == eval.TokenRequestPrefix+rtName {
+			if isTr && k == eval.TokenRequestPrefix+tr {
 				ctx.Variables[eval.BackendRequest] = v
 				break
 			} else if k == rtName {
@@ -125,7 +125,7 @@ func syncedUpstreamContext(evalCtx *eval.Context, entry *logrus.Entry) *hcl.Eval
 
 	if _, ok := ctx.Variables[eval.BackendResponses]; ok {
 		for k, v := range ctx.Variables[eval.BackendResponses].AsValueMap() {
-			if isTr && k == eval.TokenRequestPrefix+rtName {
+			if isTr && k == eval.TokenRequestPrefix+tr {
 				ctx.Variables[eval.BackendResponse] = v
 				break
 			} else if k == rtName {


Test patch:
diff --git a/config/configload/validate_test.go b/config/configload/validate_test.go
index b7430176..71044204 100644
--- a/config/configload/validate_test.go
+++ b/config/configload/validate_test.go
@@ -42,7 +42,7 @@ func Test_VerifyBodyAttributes(t *testing.T) {
 			"form_body": {Name: "form_body"},
 		}}, true},
 	} {
-		if err := verifyBodyAttributes(tc.content); !tc.expErr && err != nil {
+		if err := verifyBodyAttributes(request, tc.content); !tc.expErr && err != nil {
 			t.Errorf("Want no error, got: %v", err)
 		}
 	}
diff --git a/config/sequence/sequence_test.go b/config/sequence/sequence_test.go
index 4ec74946..7422fcb4 100644
--- a/config/sequence/sequence_test.go
+++ b/config/sequence/sequence_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 )
 
-func TestResolveSequence(t *testing.T) {
+func TestItem_ResolveSequence(t *testing.T) {
 
 	b := (&Item{Name: "b"}).Add(&Item{Name: "c"})
 
@@ -34,3 +34,148 @@ func TestResolveSequence(t *testing.T) {
 		})
 	}
 }
+
+func TestItem_Add_error(t *testing.T) {
+
+	tests := []struct {
+		name     string
+		itemFunc func(a, b, c *Item) *Item
+	}{
+		{
+			name: "circle 1",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				b.Add(a)
+				return a
+			},
+		},
+		{
+			name: "circle 2",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				b.Add(c)
+				c.Add(a)
+				return a
+			},
+		},
+		{
+			name: "add self",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(a)
+				return a
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(st *testing.T) {
+			i := &Item{Name: "i"}
+			j := &Item{Name: "j"}
+			k := &Item{Name: "k"}
+
+			defer func() { _ = recover() }()
+
+			tt.itemFunc(i, j, k)
+
+			st.Error("Missing panic")
+		})
+	}
+}
+
+func TestBackendItem_ResolveSequence(t *testing.T) {
+
+	tests := []struct {
+		name        string
+		itemFunc    func(a, b, c *Item) *Item
+		expResolved []string
+	}{
+		{
+			name: "order A",
+			itemFunc: func(a, b, c *Item) *Item {
+				b.Add(c)
+				a.Add(b)
+				a.Add(c)
+				return a
+			},
+			expResolved: []string{"vault", "as", "rs"},
+		},
+		{
+			name: "order B",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				a.Add(c)
+				b.Add(c)
+				return a
+			},
+			expResolved: []string{"vault", "as", "rs"},
+		},
+		{
+			name: "multiple",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				a.Add(b)
+				return a
+			},
+			expResolved: []string{"as", "rs"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(st *testing.T) {
+			var resolved, seen []string
+
+			rs := NewBackendItem("rs")
+			as := NewBackendItem("as")
+			vault := NewBackendItem("vault")
+
+			resolveSequence(tt.itemFunc(rs, as, vault), &resolved, &seen)
+			if diff := cmp.Diff(tt.expResolved, resolved); diff != "" {
+				st.Errorf("\ngot:\t%#v\ndiff: %s\nseen:\t%#v", resolved, diff, seen)
+			}
+		})
+	}
+}
+
+func TestBackendItem_Add_error(t *testing.T) {
+
+	tests := []struct {
+		name     string
+		itemFunc func(a, b, c *Item) *Item
+	}{
+		{
+			name: "circle 1",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				b.Add(a)
+				return a
+			},
+		},
+		{
+			name: "circle 2",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(b)
+				b.Add(c)
+				c.Add(a)
+				return a
+			},
+		},
+		{
+			name: "add self",
+			itemFunc: func(a, b, c *Item) *Item {
+				a.Add(a)
+				return a
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(st *testing.T) {
+			rs := NewBackendItem("rs")
+			as := NewBackendItem("as")
+			vault := NewBackendItem("vault")
+
+			defer func() { _ = recover() }()
+
+			tt.itemFunc(rs, as, vault)
+
+			st.Error("Missing panic")
+		})
+	}
+}
diff --git a/server/http_oauth2_test.go b/server/http_oauth2_test.go
index 8c2bd162..73ac7254 100644
--- a/server/http_oauth2_test.go
+++ b/server/http_oauth2_test.go
@@ -992,3 +992,153 @@ func TestNestedBackendOauth2(t *testing.T) {
 		}
 	}
 }
+
+func TestTokenRequest(t *testing.T) {
+	helper := test.New(t)
+
+	asOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		if req.Header.Get("KeyId") != "the-key" {
+			rw.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+
+		if user, _, _ := req.BasicAuth(); user != "the-key" {
+			rw.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+
+		reqBody, _ := io.ReadAll(req.Body)
+
+		// path_prefix context test prepends "the-key"
+
+		if req.URL.Path == "/the-key/token" {
+			expBody := "grant_type=client_credentials"
+			if expBody != string(reqBody) {
+				t.Errorf("wrong request body /token\nwant: %q\ngot:  %q", expBody, reqBody)
+			}
+			rw.Header().Set("Content-Type", "application/json")
+			rw.WriteHeader(http.StatusOK)
+
+			body := []byte(`{
+				"access_token": "tok0",
+				"token_type": "bearer",
+				"expires_in": 100
+			}`)
+			_, werr := rw.Write(body)
+			helper.Must(werr)
+
+			return
+		} else if req.URL.Path == "/the-key/token1" {
+			expBody := "client_id=clid&client_secret=cls&grant_type=client_credentials"
+			if expBody != string(reqBody) {
+				t.Errorf("wrong request body /token1\nwant: %q\ngot:  %q", expBody, reqBody)
+			}
+			rw.Header().Set("Content-Type", "application/json")
+			rw.WriteHeader(http.StatusOK)
+
+			body := []byte(`{
+				"access_token": "tok1",
+				"token_type": "bearer",
+				"expires_in": 100
+			}`)
+			_, werr := rw.Write(body)
+			helper.Must(werr)
+
+			return
+		} else if req.URL.Path == "/the-key/token2" {
+			if "foo=bar" != req.URL.RawQuery {
+				t.Errorf("wrong request URL query /token2\nwant: %q\ngot:  %q", "foo=bar", req.URL.RawQuery)
+			}
+			expBody := "client_id=clid&client_secret=cls&grant_type=password&password=asdf&username=user"
+			if expBody != string(reqBody) {
+				t.Errorf("wrong request body /token2\nwant: %q\ngot:  %q", expBody, reqBody)
+			}
+			rw.Header().Set("Content-Type", "application/json")
+			rw.WriteHeader(http.StatusOK)
+
+			body := []byte(`{
+				"access_token": "tok2",
+				"token_type": "bearer",
+				"expires_in": 100
+			}`)
+			_, werr := rw.Write(body)
+			helper.Must(werr)
+
+			return
+		}
+		rw.WriteHeader(http.StatusBadRequest)
+	}))
+	defer asOrigin.Close()
+
+	rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		if req.Header.Get("Authorization") != "Bearer tok0" ||
+			req.Header.Get("Auth-1") != "tok1" ||
+			req.Header.Get("Auth-2") != "tok2" ||
+			req.Header.Get("Auth-3") != "tok2" ||
+			req.Header.Get("Auth-4") != "tok1" ||
+			req.Header.Get("Auth-5") != "tok2" ||
+			req.Header.Get("Auth-6") != "tok2" ||
+			req.Header.Get("KeyId") != "the-key" {
+			rw.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+
+		if req.URL.Path == "/resource" {
+			rw.WriteHeader(http.StatusNoContent)
+			return
+		}
+
+		rw.WriteHeader(http.StatusNotFound)
+	}))
+	defer rsOrigin.Close()
+
+	vaultOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		if req.URL.Path == "/key" {
+			rw.WriteHeader(http.StatusOK)
+
+			body := []byte("the-key")
+			_, werr := rw.Write(body)
+			helper.Must(werr)
+
+			return
+		}
+		rw.WriteHeader(http.StatusBadRequest)
+	}))
+	defer vaultOrigin.Close()
+
+	confPath := "testdata/oauth2/token_request.hcl"
+	shutdown, hook := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"asOrigin": asOrigin.URL, "rsOrigin": rsOrigin.URL, "vaultOrigin": vaultOrigin.URL})
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/resource", nil)
+	helper.Must(err)
+	hook.Reset()
+	res, err := newClient().Do(req)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusNoContent {
+		t.Errorf("expected status %d, got: %d", http.StatusNoContent, res.StatusCode)
+	}
+}
+
+func TestTokenRequest_Error(t *testing.T) {
+	rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		rw.WriteHeader(http.StatusInternalServerError)
+	}))
+
+	helper := test.New(t)
+
+	confPath := "testdata/oauth2/token_request_error.hcl"
+	shutdown, hook := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"rsOrigin": rsOrigin.URL})
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/resource", nil)
+	helper.Must(err)
+	hook.Reset()
+	res, err := newClient().Do(req)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusNoContent {
+		t.Errorf("expected status %d, got: %d", http.StatusNoContent, res.StatusCode)
+	}
+}
diff --git a/server/testdata/oauth2/token_request.hcl b/server/testdata/oauth2/token_request.hcl
new file mode 100644
index 00000000..8831d6ea
--- /dev/null
+++ b/server/testdata/oauth2/token_request.hcl
@@ -0,0 +1,87 @@
+server "api" {
+  api {
+    endpoint "/resource" {
+      proxy {
+        backend = "be"
+      }
+    }
+  }
+}
+
+definitions {
+  backend "be" {
+    origin = "{{.rsOrigin}}"
+    set_request_headers = {
+      Auth-1 = backends.be.beta_tokens.tr1
+      Auth-2 = backends.be.beta_tokens.default
+      Auth-3 = backends.be.beta_token
+      Auth-4 = beta_backend.beta_tokens.tr1
+      Auth-5 = beta_backend.beta_tokens.default
+      Auth-6 = beta_backend.beta_token
+      KeyId = backends.as.beta_token
+    }
+
+    oauth2 {
+      token_endpoint = "{{.asOrigin}}/token"
+      backend = "as"
+      client_id = "clid"
+      client_secret = "cls"
+      grant_type = "client_credentials"
+    }
+
+    beta_token_request "tr1" {
+      url = "{{.asOrigin}}/token1"
+      backend = "as"
+      form_body = {
+        client_id = "clid"
+        client_secret = "cls"
+        grant_type = "client_credentials"
+      }
+      token = beta_token_response.json_body.access_token
+      ttl = "${default(beta_token_response.json_body.expires_in, 3600) * 0.9}s"
+    }
+
+    beta_token_request "default" {
+      url = "{{.asOrigin}}/token2"
+      query_params = {
+        foo = "bar"
+      }
+      backend = "as"
+      form_body = {
+        client_id = "clid"
+        client_secret = "cls"
+        grant_type = "password"
+        username = "user"
+        password = "asdf"
+      }
+      token = beta_token_response.json_body.access_token
+      ttl = "${default(beta_token_response.json_body.expires_in, 3600) * 0.9}s"
+    }
+  }
+
+  backend "as" {
+    origin = "{{.asOrigin}}"
+    set_request_headers = {
+      KeyId = beta_backend.beta_token
+    }
+
+    # context test for local backend variable
+    basic_auth = "${beta_backend.beta_token}:"
+    path_prefix = "/${beta_backend.beta_token}"
+
+    beta_token_request {
+      url = "{{.vaultOrigin}}/key"
+      backend = "vault"
+      token = beta_token_response.body
+      ttl = "1h"
+    }
+  }
+
+  backend "vault" {
+    origin = "{{.vaultOrigin}}"
+  }
+}
+
+settings {
+  no_proxy_from_env = true
+}
diff --git a/server/testdata/oauth2/token_request_error.hcl b/server/testdata/oauth2/token_request_error.hcl
new file mode 100644
index 00000000..1cf9b79f
--- /dev/null
+++ b/server/testdata/oauth2/token_request_error.hcl
@@ -0,0 +1,58 @@
+server "api" {
+  api {
+    endpoint "/resource" {
+      proxy {
+        backend = "be"
+      }
+
+      error_handler "beta_backend_token_request" {
+        response {
+          status = 204
+        }
+      }
+    }
+  }
+}
+
+definitions {
+  backend "be" {
+    origin = "{{.rsOrigin}}"
+
+    beta_token_request "tr1" {
+      url = "/token1"
+      backend = "down"
+      form_body = {
+        client_id = "clid"
+        client_secret = "cls"
+        grant_type = "client_credentials"
+      }
+      token = beta_token_response.json_body.access_token
+      ttl = "${default(beta_token_response.json_body.expires_in, 3600) * 0.9}s"
+    }
+
+    beta_token_request "default" {
+      url = "/token2"
+      query_params = {
+        foo = "bar"
+      }
+      backend = "down"
+      form_body = {
+        client_id = "clid"
+        client_secret = "cls"
+        grant_type = "password"
+        username = "user"
+        password = "asdf"
+      }
+      token = beta_token_response.json_body.access_token
+      ttl = "${default(beta_token_response.json_body.expires_in, 3600) * 0.9}s"
+    }
+  }
+
+  backend "down" {
+    origin = "http://1.2.3.4"
+  }
+}
+
+settings {
+  no_proxy_from_env = true
+}