Repository: avenga/couper
Pull request #130 (2021-03-05 15:59:11)

Add OAuth2 support


Patch:
diff --git a/cache/memory.go b/cache/memory.go
new file mode 100644
index 00000000..680fff09
--- /dev/null
+++ b/cache/memory.go
@@ -0,0 +1,106 @@
+package cache
+
+import (
+	"runtime/debug"
+	"sync"
+	"time"
+
+	"github.com/sirupsen/logrus"
+)
+
+const maxExpiresIn = 86400
+
+type entry struct {
+	value string
+	expAt int64
+}
+
+// MemoryStore represents the <MemoryStore> object.
+type MemoryStore struct {
+	db     map[string]*entry
+	mu     sync.RWMutex
+	log    *logrus.Entry
+	quitCh <-chan struct{}
+}
+
+// New creates a new <MemoryStore> object.
+func New(log *logrus.Entry, quitCh <-chan struct{}) *MemoryStore {
+	store := &MemoryStore{
+		db:     make(map[string]*entry),
+		log:    log,
+		quitCh: quitCh,
+	}
+
+	go store.gc()
+
+	return store
+}
+
+// Del deletes the value by the key from the <MemoryStore>.
+func (ms *MemoryStore) Del(k string) {
+	ms.mu.Lock()
+
+	delete(ms.db, k)
+
+	ms.mu.Unlock()
+}
+
+// Get return the value by the key if the ttl is not expired from the <MemoryStore>.
+func (ms *MemoryStore) Get(k string) string {
+	ms.mu.RLock()
+	defer ms.mu.RUnlock()
+
+	if v, ok := ms.db[k]; ok {
+		if time.Now().Unix() >= v.expAt {
+			return ""
+		}
+
+		return v.value
+	}
+
+	return ""
+}
+
+// Set stores a key/value pair for <ttl> second(s) into the <MemoryStore>.
+func (ms *MemoryStore) Set(k, v string, ttl int64) {
+	if ttl < 0 {
+		ttl = 0
+	} else if ttl > maxExpiresIn {
+		ttl = maxExpiresIn
+	}
+
+	ms.mu.Lock()
+	ms.db[k] = &entry{
+		value: v,
+		expAt: time.Now().Unix() + ttl,
+	}
+	ms.mu.Unlock()
+}
+
+func (ms *MemoryStore) gc() {
+	ticker := time.NewTicker(time.Second)
+
+	defer func() {
+		if rcv := recover(); rcv != nil {
+			ms.log.WithField("panic", debug.Stack()).Errorf("%v", rcv)
+		}
+		ticker.Stop()
+	}()
+
+	for {
+		select {
+		case <-ms.quitCh:
+			return
+		case now := <-ticker.C:
+			ms.mu.Lock()
+
+			for k, v := range ms.db {
+				if now.Unix() >= v.expAt {
+					delete(ms.db, k)
+				}
+			}
+
+			ms.mu.Unlock()
+		}
+	}
+}
diff --git a/command/run.go b/command/run.go
index 5a0a0e75..498c1e0d 100644
--- a/command/run.go
+++ b/command/run.go
@@ -5,9 +5,9 @@ import (
 	"flag"
 	"fmt"
 
-	"github.com/avenga/couper/config/env"
-
+	"github.com/avenga/couper/cache"
 	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/env"
 	"github.com/avenga/couper/config/runtime"
 	"github.com/avenga/couper/server"
 	"github.com/sirupsen/logrus"
@@ -49,7 +49,7 @@ func (r Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry) e
 	env.Decode(&timings)
 
 	// logEntry has still the 'daemon' type which can be used for config related load errors.
-	srvMux, err := runtime.NewServerConfiguration(config, logEntry)
+	srvMux, err := runtime.NewServerConfiguration(config, logEntry, cache.New(logEntry, r.context.Done()))
 	if err != nil {
 		return err
 	}
diff --git a/config/backend.go b/config/backend.go
index 2208c1e8..e70fe73d 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -25,6 +25,9 @@ type Backend struct {
 	Remain                 hcl.Body `hcl:",remain"`
 	TTFBTimeout            string   `hcl:"ttfb_timeout,optional"`
 	Timeout                string   `hcl:"timeout,optional"`
+
+	// explicit configuration on load
+	OAuth2 *OAuth2
 }
 
 // HCLBody implements the <Inline> interface.
diff --git a/config/configload/load.go b/config/configload/load.go
index 73b2ec86..76a5e16c 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -7,6 +7,7 @@ import (
 	"net/url"
 	"path/filepath"
 	"regexp"
+	"strings"
 
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
@@ -23,6 +24,7 @@ import (
 
 const (
 	backend     = "backend"
+	oauth2      = "oauth2"
 	definitions = "definitions"
 	nameLabel   = "name"
 	proxy       = "proxy"
@@ -34,6 +36,11 @@ const (
 )
 
 var regexProxyRequestLabel = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
+var envContext *hcl.EvalContext
+
+func init() {
+	envContext = eval.NewContext(nil).HCLContext()
+}
 
 func LoadFile(filePath string) (*config.Couper, error) {
 	_, err := startup.SetWorkingDirectory(filePath)
@@ -60,8 +67,6 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) {
 	return LoadConfig(hclBody, src, filename)
 }
 
-var envContext *hcl.EvalContext
-
 func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, error) {
 	defaults := config.DefaultSettings
 
@@ -466,7 +471,7 @@ func newBackend(definedBackends Backends, inlineConfig config.Inline, url string
 					r := inlineConfig.HCLBody().MissingItemRange()
 					return nil, hcl.Diagnostics{&hcl.Diagnostic{
 						Subject: &r,
-						Summary: "The origin of 'url' and 'backend.origin' must be equal",
+						Summary: "The host of 'url' and 'backend.origin' must be equal",
 					}}
 				}
 			}
@@ -475,6 +480,20 @@ func newBackend(definedBackends Backends, inlineConfig config.Inline, url string
 		}
 	}
 
+	oauth2Backend, err := newOAuthBackend(definedBackends, bend)
+	if err != nil {
+		return nil, err
+	}
+
+	if oauth2Backend != nil {
+		wrapped := hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
+			{Type: oauth2, Body: hclbody.New(&hcl.BodyContent{Blocks: []*hcl.Block{
+				{Type: backend, Body: oauth2Backend},
+			}})},
+		}})
+		bend = MergeBodies([]hcl.Body{bend, wrapped})
+	}
+
 	if err = validateOrigin(bend); err != nil {
 		r := inlineConfig.HCLBody().MissingItemRange()
 		return nil, hcl.Diagnostics{&hcl.Diagnostic{
@@ -486,6 +505,50 @@ func newBackend(definedBackends Backends, inlineConfig config.Inline, url string
 	return bend, nil
 }
 
+func newOAuthBackend(definedBackends Backends, parent hcl.Body) (hcl.Body, error) {
+	innerContent, err := contentByType(oauth2, parent)
+	if err != nil {
+		return nil, err
+	}
+
+	oauthBlocks := innerContent.Blocks.OfType(oauth2)
+	if len(oauthBlocks) == 0 {
+		return nil, nil
+	}
+
+	attrContent, _, diags := oauthBlocks[0].Body.PartialContent(config.OAuthEndpointSchema)
+	if diags.HasErrors() {
+		return nil, diags
+	}
+	var endpointURL string
+	if attr, ok := attrContent.Attributes[config.OAuthEndpointSchema.Attributes[0].Name]; ok {
+		value, diags := attr.Expr.Value(envContext)
+		if diags.HasErrors() {
+			return nil, diags
+		}
+		endpointURL = value.AsString()
+	}
+
+	backendContent, err := contentByType(backend, oauthBlocks[0].Body)
+	if err != nil {
+		return nil, err
+	}
+
+	oauthBackend, err := mergeBackendBodies(definedBackends, &config.Backend{Remain: hclbody.New(backendContent)})
+	b, err := newBackend(definedBackends, &config.OAuth2{Remain: hclbody.New(&hcl.BodyContent{
+		Blocks: []*hcl.Block{
+			{Type: backend, Body: oauthBackend},
+		},
+	})}, endpointURL)
+	if err != nil {
+		diags := err.(hcl.Diagnostics)
+		if strings.HasPrefix(diags[0].Summary, "The host of 'url'") {
+			diags[0].Summary = strings.Replace(diags[0].Summary, "The host of 'url'", "The host of 'token_endpoint'", 1)
+		}
+	}
+	return b, err
+}
+
 func newBackendFromURL(rawURL string) (hcl.Body, string, error) {
 	u, err := url.Parse(rawURL)
 	if err != nil {
@@ -497,7 +560,9 @@ func newBackendFromURL(rawURL string) (hcl.Body, string, error) {
 	attributes := map[string]*hcl.Attribute{
 		"name":   {Name: "name", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(defaultNameLabel)}},
 		"origin": {Name: "origin", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(origin)}},
-		"path":   {Name: "path", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(u.Path)}},
+	}
+	if u.Path != "" {
+		attributes["path"] = &hcl.Attribute{Name: "path", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal(u.Path)}}
 	}
 
 	if len(u.Query()) > 0 {
diff --git a/config/configload/merged.go b/config/configload/merged.go
index 226b3dce..c3d94054 100644
--- a/config/configload/merged.go
+++ b/config/configload/merged.go
@@ -6,9 +6,8 @@ package configload
 import (
 	"fmt"
 
-	"github.com/hashicorp/hcl/v2"
-
 	hclbody "github.com/avenga/couper/config/body"
+	"github.com/hashicorp/hcl/v2"
 )
 
 // reportDuplicates is the global switch to handle force merges.
@@ -94,27 +93,12 @@ func (mb mergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
 	for _, body := range mb {
 		thisAttrs, thisDiags := body.JustAttributes()
 
-		if len(thisDiags) != 0 {
+		if len(thisDiags) > 0 {
 			diags = append(diags, thisDiags...)
 		}
 
 		if thisAttrs != nil {
-			for name, attr := range thisAttrs {
-				if existing := attrs[name]; reportDuplicates && existing != nil {
-					diags = diags.Append(&hcl.Diagnostic{
-						Severity: hcl.DiagError,
-						Summary:  "Duplicate argument",
-						Detail: fmt.Sprintf(
-							"Argument %q was already set at %s",
-							name, existing.NameRange.String(),
-						),
-						Subject: &attr.NameRange,
-					})
-					continue
-				}
-
-				attrs[name] = attr
-			}
+			diags = append(diags, mergeAttributes(attrs, thisAttrs)...)
 		}
 	}
 
@@ -167,64 +151,57 @@ func (mb mergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl
 		if thisLeftovers != nil {
 			mergedLeftovers = append(mergedLeftovers, thisLeftovers)
 		}
-		if len(thisDiags) != 0 {
+		if len(thisDiags) > 0 {
 			diags = append(diags, thisDiags...)
 		}
 
 		if thisContent.Attributes != nil {
-			for name, attr := range thisContent.Attributes {
-				if existing := content.Attributes[name]; reportDuplicates && existing != nil {
-					diags = diags.Append(&hcl.Diagnostic{
-						Severity: hcl.DiagError,
-						Summary:  "Duplicate argument",
-						Detail: fmt.Sprintf(
-							"Argument %q was already set at %s",
-							name, existing.NameRange.String(),
-						),
-						Subject: &attr.NameRange,
-					})
-					continue
-				}
-				content.Attributes[name] = attr
-			}
+			diags = append(diags, mergeAttributes(content.Attributes, thisContent.Attributes)...)
 		}
 
 		if len(thisContent.Blocks) != 0 {
 			for _, thisContentBlock := range thisContent.Blocks {
-				if len(thisContentBlock.Labels) == 0 {
-					// assume a block definition without a label could not exist twice. Merge attrs.
-					var contentBlockType string
-					for _, contentBlock := range content.Blocks {
-						if contentBlock.Type == thisContentBlock.Type {
-							contentBlockType = contentBlock.Type
-							contentAttrs, contentAttrsDiags := contentBlock.Body.JustAttributes()
-							if contentAttrsDiags.HasErrors() {
-								diags = append(diags, contentAttrsDiags...)
-								break
-							}
-							thisContentAttrs, thisContentAttrsDiags := thisContentBlock.Body.JustAttributes()
-							if thisContentAttrsDiags.HasErrors() {
-								diags = append(diags, thisContentAttrsDiags...)
-								break
-							}
-							for name, attr := range thisContentAttrs {
-								contentAttrs[name] = attr
-							}
-							contentBlock.Body = hclbody.New(&hcl.BodyContent{
-								Attributes:       contentAttrs,
-								MissingItemRange: thisContentBlock.DefRange,
-							})
-						}
-					}
-					if contentBlockType == "" { // nothing found
-						content.Blocks = append(content.Blocks, thisContentBlock)
+				if len(thisContentBlock.Labels) > 0 {
+					content.Blocks = append(content.Blocks, thisContentBlock)
+					continue
+				}
+				// assume a block definition without a label could not exist twice. Merge attrs.
+				var contentBlockType string
+				var idx = 0
+				for i, contentBlock := range content.Blocks {
+					if contentBlock.Type == thisContentBlock.Type {
+						contentBlockType = contentBlock.Type
+						idx = i
+						break
 					}
-				} else {
+				}
+
+				if contentBlockType == "" { // nothing found
 					content.Blocks = append(content.Blocks, thisContentBlock)
+					continue
+				}
+
+				contentAttrs, contentAttrsDiags := content.Blocks[idx].Body.JustAttributes()
+				if contentAttrsDiags.HasErrors() {
+					diags = append(diags, contentAttrsDiags...)
+				}
+				thisContentAttrs, thisContentAttrsDiags := thisContentBlock.Body.JustAttributes()
+				if thisContentAttrsDiags.HasErrors() {
+					diags = append(diags, thisContentAttrsDiags...)
 				}
+
+				diags = append(diags, mergeAttributes(contentAttrs, thisContentAttrs)...)
+
+				content.Blocks[idx].Body = MergeBodies([]hcl.Body{
+					thisContentBlock.Body, // keep child blocks
+					hclbody.New(&hcl.BodyContent{
+						Attributes:       contentAttrs,
+						MissingItemRange: thisContentBlock.DefRange,
+					}),
+				})
 			}
-			//content.Blocks = append(content.Blocks, thisContent.Blocks...)
 		}
+		//content.Blocks = append(content.Blocks, thisContent.Blocks...)
 	}
 
 	// Finally, we check for required attributes.
@@ -275,3 +252,22 @@ func (mb mergedBodies) JustAllAttributesWithName(name string) []hcl.Attributes {
 	}
 	return result
 }
+
+func mergeAttributes(left, right hcl.Attributes) (diags hcl.Diagnostics) {
+	for name, attr := range right {
+		if existing := left[name]; reportDuplicates && existing != nil {
+			diags = diags.Append(&hcl.Diagnostic{
+				Severity: hcl.DiagError,
+				Summary:  "Duplicate argument",
+				Detail: fmt.Sprintf(
+					"Argument %q was already set at %s",
+					name, existing.NameRange.String(),
+				),
+				Subject: &attr.NameRange,
+			})
+			continue
+		}
+		left[name] = attr
+	}
+	return diags
+}
diff --git a/config/oauth2.go b/config/oauth2.go
new file mode 100644
index 00000000..e75aa453
--- /dev/null
+++ b/config/oauth2.go
@@ -0,0 +1,63 @@
+package config
+
+import (
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+)
+
+var _ Inline = &OAuth2{}
+
+var OAuthEndpointSchema = &hcl.BodySchema{
+	Attributes: []hcl.AttributeSchema{
+		{Name: "token_endpoint"},
+	},
+}
+
+var OAuthBlockSchema = &hcl.BodySchema{
+	Blocks: []hcl.BlockHeaderSchema{
+		{
+			Type: "oauth2",
+		},
+	},
+}
+
+// OAuth2 represents the <OAuth2> object.
+type OAuth2 struct {
+	BackendName   string   `hcl:"backend,optional"`
+	GrantType     string   `hcl:"grant_type"`
+	Remain        hcl.Body `hcl:",remain"`
+	TokenEndpoint string   `hcl:"token_endpoint,optional"`
+}
+
+// HCLBody implements the <Inline> interface.
+func (oa OAuth2) HCLBody() hcl.Body {
+	return oa.Remain
+}
+
+// Reference implements the <Inline> interface.
+func (oa OAuth2) Reference() string {
+	return "OAuth2"
+}
+
+// Schema implements the <Inline> interface.
+func (oa OAuth2) Schema(inline bool) *hcl.BodySchema {
+	if !inline {
+		schema, _ := gohcl.ImpliedBodySchema(oa)
+		return schema
+	}
+
+	type Inline struct {
+		Backend      *Backend `hcl:"backend,block"`
+		ClientID     string   `hcl:"client_id"`
+		ClientSecret string   `hcl:"client_secret"`
+	}
+
+	schema, _ := gohcl.ImpliedBodySchema(&Inline{})
+
+	// A backend reference is defined, backend block is not allowed.
+	if oa.BackendName != "" {
+		schema.Blocks = nil
+	}
+
+	return newBackendSchema(schema, oa.HCLBody())
+}
diff --git a/config/request/context_key.go b/config/request/context_key.go
index c4e26101..341dd138 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -13,6 +13,7 @@ const (
 	RoundTripName
 	RoundTripProxy
 	ServerName
+	TokenRequest
 	Wildcard
 	XFF
 )
diff --git a/config/runtime/server.go b/config/runtime/server.go
index 1385627f..eae3c22a 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -21,6 +21,7 @@ import (
 	"github.com/sirupsen/logrus"
 
 	ac "github.com/avenga/couper/accesscontrol"
+	"github.com/avenga/couper/cache"
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/runtime/server"
 	"github.com/avenga/couper/errors"
@@ -64,7 +65,9 @@ type endpointMap map[*config.Endpoint]*config.API
 
 // NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration.
 // Wire up all endpoints and maps them within the returned Server.
-func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfiguration, error) {
+func NewServerConfiguration(
+	conf *config.Couper, log *logrus.Entry, memStore *cache.MemoryStore,
+) (ServerConfiguration, error) {
 	defaultPort := conf.Settings.DefaultPort
 
 	// confCtx is created to evaluate request / response related configuration errors on start.
@@ -232,7 +235,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 			//var redirect producer.Redirect
 
 			for _, proxyConf := range endpointConf.Proxies {
-				backend, berr := newBackend(confCtx, proxyConf.Backend, log, conf.Settings.NoProxyFromEnv)
+				backend, berr := newBackend(confCtx, proxyConf.Backend, log, conf.Settings.NoProxyFromEnv, memStore)
 				if berr != nil {
 					return nil, berr
 				}
@@ -245,7 +248,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 			}
 
 			for _, requestConf := range endpointConf.Requests {
-				backend, berr := newBackend(confCtx, requestConf.Backend, log, conf.Settings.NoProxyFromEnv)
+				backend, berr := newBackend(confCtx, requestConf.Backend, log, conf.Settings.NoProxyFromEnv, memStore)
 				if berr != nil {
 					return nil, berr
 				}
@@ -328,7 +331,9 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry) (ServerConfi
 	return serverConfiguration, nil
 }
 
-func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry, ignoreProxyEnv bool) (http.RoundTripper, error) {
+func newBackend(
+	evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry,
+	ignoreProxyEnv bool, memStore *cache.MemoryStore) (http.RoundTripper, error) {
 	beConf := *DefaultBackendConf
 	if diags := gohcl.DecodeBody(backendCtx, evalCtx, &beConf); diags.HasErrors() {
 		return nil, diags
@@ -376,6 +381,31 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 	}
 	backend := transport.NewBackend(backendCtx, tc, options, log)
 
+	oauthContent, _, _ := backendCtx.PartialContent(config.OAuthBlockSchema)
+	if oauthContent == nil {
+		return backend, nil
+	}
+
+	if blocks := oauthContent.Blocks.OfType("oauth2"); len(blocks) > 0 {
+		beConf.OAuth2 = &config.OAuth2{}
+
+		if diags := gohcl.DecodeBody(blocks[0].Body, evalCtx, beConf.OAuth2); diags.HasErrors() {
+			return nil, diags
+		}
+
+		innerContent, _, diags := beConf.OAuth2.Remain.PartialContent(beConf.OAuth2.Schema(true))
+		if diags.HasErrors() {
+			return nil, diags
+		}
+		innerBackend := innerContent.Blocks.OfType("backend")[0] // backend block is set by configload
+		authBackend, authErr := newBackend(evalCtx, innerBackend.Body, log, ignoreProxyEnv, memStore)
+		if authErr != nil {
+			return nil, authErr
+		}
+
+		return transport.NewOAuth2(beConf.OAuth2, memStore, authBackend, backend)
+	}
+
 	return backend, nil
 }
 
diff --git a/docs/README.md b/docs/README.md
index 739f6c7c..cea14248 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -27,6 +27,7 @@
     * [Endpoint Block](#endpoint-block)
     * [Backend Block](#backend-block)
       * [OpenAPI Block](#openapi-block)
+      * [OAuth2 Block](#oauth2-block)
       * [Transport Settings Attributes](#transport-settings-attributes)
     * [CORS Block](#cors-block)
     * [Access Control](#access-control)
@@ -503,6 +504,7 @@ as reference.
 | *label*                         | &#9888; Mandatory in the [Definitions Block](#definitions-block). |
 | **Nested blocks**               | **Description** |
 | [OpenAPI Block](#openapi-block) | <ul><li>Optional.</li><li>Definition for validating outgoing requests to the origin and incoming responses from the origin.</li></ul> |
+| [OAuth2 Block](#oauth2-block)   | <ul><li>Optional.</li><li>OAuth2 configuration block.</li></ul>
 | **Attributes**                  | **Description** |
 | `basic_auth`                    | <ul><li>Optional.</li><li>Basic auth for the upstream request in format `username:password`.</li></ul> |
 | `hostname`                      | <ul><li>Optional.</li><li>Value of the HTTP host header field for the origin request. Since `hostname` replaces the request host the value will also be used for a server identity check during a TLS handshake with the origin.</li></ul> |
@@ -606,6 +608,21 @@ The CORS block configures the CORS (Cross-Origin Resource Sharing) behavior in C
 | `disable`           | <ul><li>Optional.</li><li>Set to `true` to disable the inheritance of CORS from the [Server Block](#server-block) in [Files Block](#files-block), [SPA Block](#spa-block) and [API Block](#api-block) contexts.</li><li>Default `false`.</li></ul> |
 | `max_age`           | <ul><li>Optional.</li><li>Indicates the time the information provided by the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` response HTTP header fields.</li><li>Can be cached (string with time unit, e.g. `"1h"`).</li></li></ul> |
 
+### OAuth2 Block
+
+| Block                           | Description |
+|:--------------------------------|:------------|
+| *context*                       | [Backend Block](#backend-block). |
+| *label*                         | Not implemented. |
+| **Nested blocks**               | **Description** |
+| [Backend Block](#backend-block) | Optional. |
+| **Attributes**                  | **Description** |
+| `backend`                       | <ul><li>Optional.</li><li>[Backend Block Reference](#backend-block-reference).</li></ul> |
+| `grant_type`                    | <ul><li>&#9888; Mandatory.</li><li>Available values: `client_credentials`.</li></ul> |
+| `token_endpoint`                | <ul><li>&#9888; Mandatory.</li><li>URL of the token endpoint at the authorization server.</li></ul> |
+| `client_id`                     | <ul><li>&#9888; Mandatory.</li><li>The client identifier.</li></ul> |
+| `client_secret`                 | <ul><li>&#9888; Mandatory.</li><li>The client password.</li></ul> |
+
 ### Modifier
 
 * [Query Parameter](#query-parameter)
diff --git a/errors/code.go b/errors/code.go
index 74c0b165..4483232d 100644
--- a/errors/code.go
+++ b/errors/code.go
@@ -31,6 +31,9 @@ const (
 	AuthorizationRequired Code = 5000 + iota
 	AuthorizationFailed
 	BasicAuthFailed
+	TokenRequestFailed
+	MissingOAuth2Credentials
+	MissingOAuth2AccessToken
 )
 
 const (
@@ -66,9 +69,12 @@ var codes = map[Code]string{
 	APIConnect:       "API upstream connection error",
 	APIProxyConnect:  "upstream connection error via configured proxy",
 	// 5xxx
-	AuthorizationRequired: "Authorization required",
-	AuthorizationFailed:   "Authorization failed",
-	BasicAuthFailed:       "Unauthorized",
+	AuthorizationRequired:    "Authorization required",
+	AuthorizationFailed:      "Authorization failed",
+	BasicAuthFailed:          "Unauthorized",
+	TokenRequestFailed:       "Token request failed",
+	MissingOAuth2Credentials: "Missing OAuth2 credentials",
+	MissingOAuth2AccessToken: "Missing OAuth2 access token",
 	// 6xxx
 	UpstreamRequestValidationFailed:  "Upstream request validation failed",
 	UpstreamResponseValidationFailed: "Upstream response validation failed",
diff --git a/handler/producer/request.go b/handler/producer/request.go
index b3a46de0..021ff998 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -17,10 +17,9 @@ type Request struct {
 	Backend http.RoundTripper
 	Body    string
 	Context hcl.Body
-	// Dispatch bool
-	Method string
-	Name   string // label
-	URL    string
+	Method  string
+	Name    string // label
+	URL     string
 }
 
 // Requests represents the producer <Requests> object.
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index 7cf4e144..16465a2c 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -46,8 +46,6 @@ type Backend struct {
 	options          *BackendOptions
 	transportConf    *Config
 	upstreamLog      *logging.UpstreamLog
-	// oauth
-	// ...
 	// TODO: OrderedList for origin AC, middlewares etc.
 }
 
@@ -92,8 +90,6 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 		return nil, err
 	}
 
-	// oauth ....
-
 	if b.options != nil && b.options.BasicAuth != "" {
 		auth := base64.StdEncoding.EncodeToString([]byte(b.options.BasicAuth))
 		req.Header.Set("Authorization", "Basic "+auth)
diff --git a/handler/transport/oauth2.go b/handler/transport/oauth2.go
new file mode 100644
index 00000000..1542a222
--- /dev/null
+++ b/handler/transport/oauth2.go
@@ -0,0 +1,193 @@
+package transport
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	"github.com/avenga/couper/cache"
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/request"
+	couperErr "github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/internal/seetie"
+)
+
+var _ http.RoundTripper = &OAuth2{}
+
+// OAuth2 represents the transport <OAuth2> object.
+type OAuth2 struct {
+	backend  http.RoundTripper
+	config   *config.OAuth2
+	memStore *cache.MemoryStore
+	next     http.RoundTripper
+}
+
+type OAuth2Credentials struct {
+	ClientID     string
+	ClientSecret string
+	StorageKey   string
+}
+
+// NewOAuth2 creates a new <http.RoundTripper> object.
+func NewOAuth2(config *config.OAuth2, memStore *cache.MemoryStore,
+	backend, next http.RoundTripper) (http.RoundTripper, error) {
+	if config.GrantType != "client_credentials" {
+		return nil, fmt.Errorf("the grant_type has to be set to 'client_credentials'")
+	}
+
+	return &OAuth2{
+		backend:  backend,
+		config:   config,
+		memStore: memStore,
+		next:     next,
+	}, nil
+}
+
+// RoundTrip implements the <http.RoundTripper> interface.
+func (oa *OAuth2) RoundTrip(req *http.Request) (*http.Response, error) {
+	credentials, err := oa.getCredentials(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if data := oa.memStore.Get(credentials.StorageKey); data != "" {
+		token, err := oa.readAccessToken(data)
+		if err != nil {
+			return nil, err
+		}
+
+		req.Header.Set("Authorization", "Bearer "+token)
+
+		return oa.next.RoundTrip(req)
+	}
+
+	tokenReq, err := oa.newTokenRequest(req.Context(), credentials)
+	if err != nil {
+		return nil, err
+	}
+
+	tokenRes, err := oa.backend.RoundTrip(tokenReq)
+	if err != nil {
+		return nil, err
+	}
+
+	if tokenRes.StatusCode != http.StatusOK {
+		return nil, couperErr.TokenRequestFailed
+	}
+
+	tokenResBytes, err := ioutil.ReadAll(tokenRes.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	token, err := oa.updateAccessToken(string(tokenResBytes), credentials.StorageKey)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Authorization", "Bearer "+token)
+
+	res, err := oa.next.RoundTrip(req)
+
+	if res != nil && res.StatusCode == http.StatusUnauthorized {
+		oa.memStore.Del(credentials.StorageKey)
+	}
+
+	return res, err
+}
+
+func (oa *OAuth2) getCredentials(req *http.Request) (*OAuth2Credentials, error) {
+	content, _, diags := oa.config.Remain.PartialContent(oa.config.Schema(true))
+	if diags.HasErrors() {
+		return nil, diags
+	}
+
+	evalContext, _ := req.Context().Value(eval.ContextType).(*eval.Context)
+
+	id, idOK := content.Attributes["client_id"]
+	idv, _ := id.Expr.Value(evalContext.HCLContext())
+	clientID := seetie.ValueToString(idv)
+
+	secret, secretOK := content.Attributes["client_secret"]
+	secretv, _ := secret.Expr.Value(evalContext.HCLContext())
+	clientSecret := seetie.ValueToString(secretv)
+
+	if !idOK || !secretOK {
+		return nil, couperErr.MissingOAuth2Credentials
+	}
+
+	return &OAuth2Credentials{
+		ClientID:     clientID,
+		ClientSecret: clientSecret,
+		// Backend is build up via config and token_endpoint will configure the backend,
+		// use the backend memory location here.
+		StorageKey: fmt.Sprintf("%p|%s|%s", &oa.backend, clientID, clientSecret),
+	}, nil
+}
+
+func (oa *OAuth2) newTokenRequest(ctx context.Context, creds *OAuth2Credentials) (*http.Request, error) {
+	post := "grant_type=" + oa.config.GrantType
+	body := ioutil.NopCloser(strings.NewReader(post))
+
+	outreq, err := http.NewRequest("POST", oa.config.TokenEndpoint, body)
+	if err != nil {
+		return nil, err
+	}
+
+	auth := base64.StdEncoding.EncodeToString([]byte(creds.ClientID + ":" + creds.ClientSecret))
+	outreq.Header.Set("Authorization", "Basic "+auth)
+	outreq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	outCtx := context.WithValue(ctx, request.TokenRequest, "oauth2")
+	return outreq.WithContext(outCtx), nil
+}
+
+func (oa *OAuth2) readAccessToken(data string) (string, error) {
+	var jData map[string]interface{}
+
+	err := json.Unmarshal([]byte(data), &jData)
+	if err != nil {
+		return "", err
+	}
+
+	var token string
+	if t, ok := jData["access_token"].(string); ok {
+		token = t
+	} else {
+		return "", couperErr.MissingOAuth2AccessToken
+	}
+
+	return token, nil
+}
+
+func (oa *OAuth2) updateAccessToken(jsonString, key string) (string, error) {
+	var jData map[string]interface{}
+
+	err := json.Unmarshal([]byte(jsonString), &jData)
+	if err != nil {
+		return "", err
+	}
+
+	var token string
+	if t, ok := jData["access_token"].(string); ok {
+		token = t
+	} else {
+		return "", couperErr.MissingOAuth2AccessToken
+	}
+
+	if oa.memStore != nil {
+		var ttl int64
+		if t, ok := jData["expires_in"].(float64); ok {
+			ttl = (int64)(t * 0.9)
+		}
+
+		oa.memStore.Set(key, jsonString, ttl)
+	}
+
+	return token, nil
+}
diff --git a/logging/upstream_log.go b/logging/upstream_log.go
index 78c58d3a..21818b8c 100644
--- a/logging/upstream_log.go
+++ b/logging/upstream_log.go
@@ -104,6 +104,10 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
 	requestFields["proto"] = req.Proto
 	requestFields["scheme"] = req.URL.Scheme
 
+	if tr, ok := req.Context().Value(request.TokenRequest).(string); ok && tr != "" {
+		fields["token_request"] = tr
+	}
+
 	fields["realtime"] = roundMS(rtDone.Sub(rtStart))
 
 	fields["status"] = 0
diff --git a/server/http.go b/server/http.go
index 128b21af..6a6d3ce3 100644
--- a/server/http.go
+++ b/server/http.go
@@ -39,7 +39,9 @@ type HTTPServer struct {
 }
 
 // NewServerList creates a list of all configured HTTP server.
-func NewServerList(cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
+func NewServerList(
+	cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings,
+	timings *runtime.HTTPTimings, srvConf runtime.ServerConfiguration) ([]*HTTPServer, func()) {
 	var list []*HTTPServer
 
 	for port, srvMux := range srvConf {
@@ -55,7 +57,9 @@ func NewServerList(cmdCtx context.Context, evalCtx *eval.Context, log logrus.Fie
 }
 
 // New creates a configured HTTP server.
-func New(cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings, timings *runtime.HTTPTimings, p runtime.Port, muxOpts *runtime.MuxOptions) *HTTPServer {
+func New(
+	cmdCtx context.Context, evalCtx *eval.Context, log logrus.FieldLogger, settings *config.Settings,
+	timings *runtime.HTTPTimings, p runtime.Port, muxOpts *runtime.MuxOptions) *HTTPServer {
 	var uidFn func() string
 	if settings.RequestIDFormat == "uuid4" {
 		uidFn = func() string {


Test patch:
diff --git a/cache/memory_test.go b/cache/memory_test.go
new file mode 100644
index 00000000..37a40312
--- /dev/null
+++ b/cache/memory_test.go
@@ -0,0 +1,40 @@
+package cache_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/avenga/couper/cache"
+)
+
+func TestCache_All(t *testing.T) {
+	ms := cache.New(nil, nil)
+
+	if v := ms.Get("key"); v != "" {
+		t.Errorf("Empty string expected, given %q", v)
+	}
+
+	go ms.Set("key", "val", 2)
+	go ms.Set("del", "del", 30)
+	go func() {
+		ms.Get("key")
+	}()
+
+	time.Sleep(300 * time.Millisecond)
+
+	if v := ms.Get("key"); v != "val" {
+		t.Errorf("Expected 'val', given %q", v)
+	}
+	if v := ms.Get("del"); v != "del" {
+		t.Errorf("Expected 'del', given %q", v)
+	}
+
+	time.Sleep(1700 * time.Millisecond)
+
+	if v := ms.Get("key"); v != "" {
+		t.Errorf("Empty string expected, given %q", v)
+	}
+	if v := ms.Get("del"); v != "del" {
+		t.Errorf("Expected 'del', given %q", v)
+	}
+}
diff --git a/config/configload/merged_test.go b/config/configload/merged_test.go
new file mode 100644
index 00000000..a14698d6
--- /dev/null
+++ b/config/configload/merged_test.go
@@ -0,0 +1,121 @@
+package configload_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/hashicorp/hcl/v2/hclwrite"
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/avenga/couper/config"
+	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/eval"
+)
+
+func TestMergeBodies(t *testing.T) {
+	type expectedBody struct {
+		OAuth2 *config.OAuth2 `hcl:"oauth2,block"`
+	}
+
+	type container struct {
+		Block []*expectedBody `hcl:"block,block"`
+	}
+
+	bodies := &container{[]*expectedBody{
+		{OAuth2: &config.OAuth2{
+			BackendName:   "test",
+			GrantType:     "no_creds",
+			TokenEndpoint: "http://this",
+		}},
+		{OAuth2: &config.OAuth2{
+			TokenEndpoint: "http://that",
+		}},
+	}}
+
+	expectedAttributes := map[string]*hcl.Attribute{
+		"backend":        {Name: "backend", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("test")}},
+		"grant_type":     {Name: "grant_type", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("no_creds")}},
+		"token_endpoint": {Name: "token_endpoint", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("http://that")}},
+	}
+
+	var hclBodies []hcl.Body
+
+	// could be done with hclmock package, but for example purposes encode and read
+	target := hclwrite.NewEmptyFile()
+	gohcl.EncodeIntoBody(bodies, target.Body())
+	// write config reference
+	//buf := &bytes.Buffer{}
+	//_, _ = target.WriteTo(buf)
+
+	configBytes := []byte(`
+block {
+
+  oauth2 {
+    backend        = "test"
+    grant_type     = "no_creds"
+    token_endpoint = "http://this"
+  }
+}
+block {
+
+  oauth2 {
+    token_endpoint = "http://that"
+  }
+}`)
+
+	conf, diags := hclsyntax.ParseConfig(configBytes, "testcase.hcl", hcl.InitialPos)
+	if diags.HasErrors() {
+		t.Error(diags)
+	}
+	defer func() {
+		if t.Failed() {
+			println(string(configBytes))
+		}
+	}()
+
+	schema, _ := gohcl.ImpliedBodySchema(&container{})
+	content, _, diags := conf.Body.PartialContent(schema)
+	if diags.HasErrors() {
+		t.Error(diags)
+	}
+	for _, inner := range content.Blocks.OfType("block") {
+		hclBodies = append(hclBodies, inner.Body)
+	}
+
+	schema, _ = gohcl.ImpliedBodySchema(bodies.Block[0])
+	content, _, diags = configload.MergeBodies(hclBodies[:]).PartialContent(schema)
+	if diags.HasErrors() {
+		t.Error(diags)
+	}
+
+	oauthBlockContent := content.Blocks.OfType("oauth2")[0]
+	resultAttributes, diags := oauthBlockContent.Body.JustAttributes()
+	if diags.HasErrors() {
+		t.Error(diags)
+	}
+
+	hclcontext := eval.NewContext(nil).HCLContext()
+	for k, attr := range expectedAttributes {
+		a, exist := resultAttributes[k]
+		if !exist {
+			t.Errorf("missing attribute %q", k)
+			continue
+		}
+
+		expVal, diags := attr.Expr.Value(hclcontext)
+		if diags.HasErrors() {
+			t.Error(diags)
+		}
+
+		val, diags := a.Expr.Value(hclcontext)
+		if diags.HasErrors() {
+			t.Error(diags)
+		}
+
+		if expVal.AsString() != val.AsString() {
+			t.Errorf("Want: %q, got %q", expVal.AsString(), val.AsString())
+		}
+	}
+}
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 1afbcfd9..a2156c92 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -3,9 +3,12 @@ package server_test
 import (
 	"encoding/json"
 	"io/ioutil"
+	"net"
 	"net/http"
+	"net/http/httptest"
 	"path"
 	"testing"
+	"time"
 
 	"github.com/avenga/couper/internal/test"
 )
@@ -118,3 +121,59 @@ func TestEndpoints_UpstreamBasicAuthAndXFF(t *testing.T) {
 		t.Errorf("Unexpected XFF header given '%s'", v)
 	}
 }
+
+func TestEndpoints_OAuth2(t *testing.T) {
+	helper := test.New(t)
+	seenCh := make(chan struct{})
+
+	origin := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		if req.URL.Path == "/oauth2" {
+			rw.Header().Set("Content-Type", "application/json")
+			rw.WriteHeader(http.StatusOK)
+
+			body := []byte(`{
+				"access_token": "abcdef0123456789",
+				"token_type": "bearer",
+				"expires_in": 100
+			}`)
+			_, werr := rw.Write(body)
+			helper.Must(werr)
+			return
+		}
+
+		if req.URL.Path == "/resource" {
+			if req.Header.Get("Authorization") == "Bearer abcdef0123456789" {
+				rw.WriteHeader(http.StatusNoContent)
+
+				go func() {
+					seenCh <- struct{}{}
+				}()
+
+				return
+			}
+		}
+
+		rw.WriteHeader(http.StatusBadRequest)
+	}))
+	ln, err := net.Listen("tcp4", testProxyAddr[7:])
+	helper.Must(err)
+	origin.Listener = ln
+	origin.Start()
+	defer origin.Close()
+	confPath := "testdata/endpoints/04_couper.hcl"
+	shutdown, _ := newCouper(confPath, test.New(t))
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/", nil)
+	helper.Must(err)
+
+	_, err = newClient().Do(req)
+	helper.Must(err)
+
+	timer := time.NewTimer(time.Second)
+	select {
+	case <-timer.C:
+		t.Error("OAuth2 request failed")
+	case <-seenCh:
+	}
+}
diff --git a/server/http_test.go b/server/http_test.go
index d71a8f1d..f3086dbb 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -15,14 +15,12 @@ import (
 	"text/template"
 	"time"
 
-	"github.com/avenga/couper/eval"
+	logrustest "github.com/sirupsen/logrus/hooks/test"
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload"
-
-	logrustest "github.com/sirupsen/logrus/hooks/test"
-
 	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/internal/test"
 	"github.com/avenga/couper/server"
 )
@@ -66,7 +64,7 @@ func TestHTTPServer_ServeHTTP_Files(t *testing.T) {
 	helper.Must(err)
 	conf.Settings.DefaultPort = 0
 
-	srvConf, err := runtime.NewServerConfiguration(conf, log.WithContext(nil))
+	srvConf, err := runtime.NewServerConfiguration(conf, log.WithContext(nil), nil)
 	helper.Must(err)
 
 	spaContent, err := ioutil.ReadFile(conf.Servers[0].Spa.BootstrapFile)
@@ -162,7 +160,7 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
 	spaContent, err := ioutil.ReadFile(conf.Servers[0].Spa.BootstrapFile)
 	helper.Must(err)
 
-	srvConf, err := runtime.NewServerConfiguration(conf, log.WithContext(nil))
+	srvConf, err := runtime.NewServerConfiguration(conf, log.WithContext(nil), nil)
 	helper.Must(err)
 
 	couper := server.New(ctx, conf.Context, log.WithContext(ctx), conf.Settings, &runtime.DefaultTimings, runtime.Port(0), srvConf[0])
diff --git a/server/testdata/endpoints/04_couper.hcl b/server/testdata/endpoints/04_couper.hcl
new file mode 100644
index 00000000..0dd48e3e
--- /dev/null
+++ b/server/testdata/endpoints/04_couper.hcl
@@ -0,0 +1,33 @@
+server "api" {
+  error_file = "./../integration/server_error.html"
+
+  api {
+    error_file = "./../integration/api_error.json"
+
+    endpoint "/" {
+      proxy {
+        backend {
+          origin = "http://example.com"
+		  path   = "/resource"
+
+		  oauth2 {
+            token_endpoint = "http://example.com/oauth2"
+            client_id      = "user"
+            client_secret  = "pass"
+            grant_type     = "client_credentials"
+
+            backend {}
+          }
+        }
+      }
+    }
+  }
+}
+
+definitions {
+  # backend origin within a definition block gets replaced with the integration test "anything" server.
+  backend "anything" {
+    path = "/anything"
+    origin = env.COUPER_TEST_BACKEND_ADDR
+  }
+}