Repository: avenga/couper
Pull request #555 (2022-08-10 15:05:43)

New grant types
`grant_type`s password and jwt-bearer for `oauth2` block

---
<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/CHANGELOG.md b/CHANGELOG.md
index d8f668a7..cb87f823 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
 * **Added**
   * [`environment` block](https://docs.couper.io/configuration/block/environment), [setting](https://docs.couper.io/configuration/block/settings) and [`couper.environment` variable](https://docs.couper.io/configuration/variables#couper) ([#521](https://github.com/avenga/couper/pull/521), ([#534](https://github.com/avenga/couper/pull/534), [#545](https://github.com/avenga/couper/pull/545))
   * used go version in `version` command ([#552](https://github.com/avenga/couper/pull/552))
+  * new `grant_type`s `"password"` and `"urn:ietf:params:oauth:grant-type:jwt-bearer"` with related attributes for [`oauth2` block](https://docs.couper.io/configuration/block/oauth2) ([#555](https://github.com/avenga/couper/pull/555))
   * [`beta_token_request` block](https://docs.couper.io/configuration/block/token_request), [`backend`](https://docs.couper.io/configuration/variables#backend) and [`beta_token_response`](https://docs.couper.io/configuration/variables#beta_token_response) variables and `beta_token(s)` properties of [`backends` variable](https://docs.couper.io/configuration/variables#backends) ([#517](https://github.com/avenga/couper/pull/517))
 
 * **Changed**
diff --git a/config/ac_oauth2.go b/config/ac_oauth2.go
index d5eeb452..2775f427 100644
--- a/config/ac_oauth2.go
+++ b/config/ac_oauth2.go
@@ -83,6 +83,10 @@ func (oa *OAuth2AC) GetName() string {
 	return oa.Name
 }
 
+func (oa *OAuth2AC) ClientAuthenticationRequired() bool {
+	return true
+}
+
 func (oa *OAuth2AC) GetClientID() string {
 	return oa.ClientID
 }
diff --git a/config/ac_oidc.go b/config/ac_oidc.go
index 16a60147..edfa6712 100644
--- a/config/ac_oidc.go
+++ b/config/ac_oidc.go
@@ -114,6 +114,10 @@ func (o *OIDC) GetName() string {
 	return o.Name
 }
 
+func (o *OIDC) ClientAuthenticationRequired() bool {
+	return true
+}
+
 func (o *OIDC) GetClientID() string {
 	return o.ClientID
 }
diff --git a/config/oauth2.go b/config/oauth2.go
index 899c4bbc..8aacbbc6 100644
--- a/config/oauth2.go
+++ b/config/oauth2.go
@@ -25,6 +25,7 @@ type OidcAS interface {
 // OAuth2Client represents the client configuration for OAuth2 clients.
 type OAuth2Client interface {
 	Inline
+	ClientAuthenticationRequired() bool
 	GetClientID() string
 	GetClientSecret() string
 	GetTokenEndpointAuthMethod() *string
diff --git a/config/oauth2ra.go b/config/oauth2ra.go
index e09e2266..6d55559c 100644
--- a/config/oauth2ra.go
+++ b/config/oauth2ra.go
@@ -5,6 +5,12 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
+const (
+	ClientCredentials = "client_credentials"
+	JwtBearer         = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+	Password          = "password"
+)
+
 var OAuthBlockSchema = &hcl.BodySchema{
 	Blocks: []hcl.BlockHeaderSchema{
 		{
@@ -22,17 +28,18 @@ var (
 
 // OAuth2ReqAuth represents the oauth2 block in a backend block.
 type OAuth2ReqAuth struct {
-	BackendName             string   `hcl:"backend,optional"`
-	ClientID                string   `hcl:"client_id"`
-	ClientSecret            string   `hcl:"client_secret"`
-	GrantType               string   `hcl:"grant_type"`
-	Password                string   `hcl:"password,optional"` // password undocumented feature!
-	Remain                  hcl.Body `hcl:",remain"`
-	Retries                 *uint8   `hcl:"retries,optional"`
-	Scope                   string   `hcl:"scope,optional"`
-	TokenEndpoint           string   `hcl:"token_endpoint,optional"`
-	TokenEndpointAuthMethod *string  `hcl:"token_endpoint_auth_method,optional"`
-	Username                string   `hcl:"username,optional"` // username undocumented feature!
+	AssertionExpr           hcl.Expression `hcl:"assertion,optional"`
+	BackendName             string         `hcl:"backend,optional"`
+	ClientID                string         `hcl:"client_id,optional"`
+	ClientSecret            string         `hcl:"client_secret,optional"`
+	GrantType               string         `hcl:"grant_type"`
+	Password                string         `hcl:"password,optional"`
+	Remain                  hcl.Body       `hcl:",remain"`
+	Retries                 *uint8         `hcl:"retries,optional"`
+	Scope                   string         `hcl:"scope,optional"`
+	TokenEndpoint           string         `hcl:"token_endpoint,optional"`
+	TokenEndpointAuthMethod *string        `hcl:"token_endpoint_auth_method,optional"`
+	Username                string         `hcl:"username,optional"`
 }
 
 // Reference implements the <BackendReference> interface.
@@ -71,6 +78,10 @@ func (oa *OAuth2ReqAuth) Schema(inline bool) *hcl.BodySchema {
 	return schema
 }
 
+func (oa *OAuth2ReqAuth) ClientAuthenticationRequired() bool {
+	return oa.GrantType != JwtBearer
+}
+
 func (oa *OAuth2ReqAuth) GetClientID() string {
 	return oa.ClientID
 }
diff --git a/config/runtime/backend.go b/config/runtime/backend.go
index 05d00903..bd3ea819 100644
--- a/config/runtime/backend.go
+++ b/config/runtime/backend.go
@@ -40,7 +40,7 @@ func NewBackend(ctx *hcl.EvalContext, body hcl.Body, log *logrus.Entry,
 
 	b, err = newBackend(ctx, body, log, conf, store)
 	if err != nil {
-		return nil, err
+		return nil, errors.Configuration.Label(name).With(err)
 	}
 
 	// to prevent weird debug sessions; max to set the internal memStore ttl limit.
@@ -76,6 +76,7 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 
 	if len(beConf.RateLimits) > 0 {
 		if strings.HasPrefix(beConf.Name, "anonymous_") {
+			// TODO remove " (%q)"?
 			return nil, fmt.Errorf("anonymous backend (%q) cannot define 'beta_rate_limit' block(s)", beConf.Name)
 		}
 
@@ -180,7 +181,7 @@ func newRequestAuthorizer(evalCtx *hcl.EvalContext, block *hcl.Block, beConf *co
 
 	switch impl := authorizerConfig.(type) {
 	case *config.OAuth2ReqAuth:
-		return transport.NewOAuth2ReqAuth(impl, memStore, authorizerBackend)
+		return transport.NewOAuth2ReqAuth(evalCtx, impl, memStore, authorizerBackend)
 	case *config.TokenRequest:
 		reqs := producer.Requests{&producer.Request{
 			Backend: authorizerBackend,
diff --git a/docs/website/content/2.configuration/4.block/oauth2req_auth.md b/docs/website/content/2.configuration/4.block/oauth2req_auth.md
index ae4be049..a1cb7f71 100644
--- a/docs/website/content/2.configuration/4.block/oauth2req_auth.md
+++ b/docs/website/content/2.configuration/4.block/oauth2req_auth.md
@@ -1,20 +1,25 @@
 # OAuth2
 
-The `oauth2` block in the [Backend Block](backend) context configures the OAuth2 Client Credentials flow to request a bearer token for the backend request.
+The `oauth2` block in the [Backend Block](backend) context configures an OAuth2 flow to request a bearer token for the backend request.
 
-| Block name | Context                         | Label    | Nested block(s)                 |
-|:-----------|:--------------------------------|:---------|:--------------------------------|
+**Note:** The token received from the authorization server's token endpoint is stored **per backend**. So even with flows where a user's account characteristics like username/password or email address are involved, there is no way to "switch" from one user to another depending on the client request.
+
+| Block name | Context                  | Label    | Nested block(s)          |
+|:-----------|:-------------------------|:---------|:-------------------------|
 | `oauth2`   | [Backend Block](backend) | no label | [Backend Block](backend) |
 
-| Attribute(s)                 | Type    | Default                 | Description                                                                                                                     | Characteristic(s)                                                                                                                                                                                 | Example                             |
-|:-----------------------------|:--------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------|
-| `backend`                    | string  | -                       | [Backend Block Reference](backend)                                                                                       | -                                                                                                                                                                                                 | -                                   |
-| `grant_type`                 | string  | -                       | -                                                                                                                               | &#9888; required, to be set to: `client_credentials`                                                                                                                                              | `grant_type = "client_credentials"` |
-| `token_endpoint`             | string  | -                       | URL of the token endpoint at the authorization server.                                                                          | &#9888; required                                                                                                                                                                                  | -                                   |
-| `client_id`                  | string  | -                       | The client identifier.                                                                                                          | &#9888; required                                                                                                                                                                                  | -                                   |
-| `client_secret`              | string  | -                       | The client password.                                                                                                            | &#9888; required.                                                                                                                                                                                 | -                                   |
-| `retries`                    | integer | `1`                     | The number of retries to get the token and resource, if the resource-request responds with `401 Unauthorized` HTTP status code. | -                                                                                                                                                                                                 | -                                   |
-| `token_endpoint_auth_method` | string  | `"client_secret_basic"` | Defines the method to authenticate the client at the token endpoint.                                                            | If set to `"client_secret_post"`, the client credentials are transported in the request body. If set to `"client_secret_basic"`, the client credentials are transported via Basic Authentication. | -                                   |
-| `scope`                      | string  | -                       | A space separated list of requested scope values for the access token.                                                          | -                                                                                                                                                                                                 | `scope = "read write"`              |
+| Attribute(s)                 | Type    | Default                 | Description                                                                                                                     | Characteristic(s)                                                                                                                                                                                 | Example                                 |
+|:-----------------------------|:--------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------|
+| `backend`                    | string  | -                       | [Backend Block Reference](backend)                                                                                              | -                                                                                                                                                                                                 | -                                       |
+| `grant_type`                 | string  | -                       | -                                                                                                                               | &#9888; required, valid values: `"client_credentials"`, `"password"`, `"urn:ietf:params:oauth:grant-type:jwt-bearer"`                                                                             | `grant_type = "client_credentials"`     |
+| `token_endpoint`             | string  | -                       | URL of the token endpoint at the authorization server.                                                                          | &#9888; required                                                                                                                                                                                  | -                                       |
+| `client_id`                  | string  | -                       | The client identifier.                                                                                                          | &#9888; required unless the client is authenticated via the JWT assertion with `grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"`.                                                      | -                                       |
+| `client_secret`              | string  | -                       | The client password.                                                                                                            | &#9888; required unless the client is authenticated via the JWT assertion with `grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"`.                                                      | -                                       |
+| `retries`                    | integer | `1`                     | The number of retries to get the token and resource, if the resource-request responds with `401 Unauthorized` HTTP status code. | -                                                                                                                                                                                                 | -                                       |
+| `token_endpoint_auth_method` | string  | `"client_secret_basic"` | Defines the method to authenticate the client at the token endpoint.                                                            | If set to `"client_secret_post"`, the client credentials are transported in the request body. If set to `"client_secret_basic"`, the client credentials are transported via Basic Authentication. | -                                       |
+| `scope`                      | string  | -                       | A space separated list of requested scope values for the access token.                                                          | -                                                                                                                                                                                                 | `scope = "read write"`                  |
+| `username`                   | string  | -                       | The (service account's) username (for password flow).                                                                           | &#9888; required if `grant_type` is `"password"`.                                                                                                                                                 | `username = env.SERVICE_ACCOUNT_USER`   |
+| `password`                   | string  | -                       | The (service account's) password (for password flow).                                                                           | &#9888; required if `grant_type` is `"password"`.                                                                                                                                                 | `username = env.SERVICE_ACCOUNT_PASSWD` |
+| `assertion`                  | string  | -                       | The assertion (JWT for jwt-bearer flow).                                                                                        | &#9888; required if `grant_type` is `"urn:ietf:params:oauth:grant-type:jwt-bearer"`.                                                                                                              | `assertion = jwt_sign("sp", {})`        |
 
 The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](../modifiers#request-header) in a [backend block](backend).
diff --git a/errors/error.go b/errors/error.go
index e8466682..ba1bb0cf 100644
--- a/errors/error.go
+++ b/errors/error.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+
+	"github.com/hashicorp/hcl/v2"
 )
 
 type Error struct {
@@ -132,14 +134,18 @@ func (e *Error) Unwrap() error {
 
 // LogError contains additional context which should be used for logging purposes only.
 func (e *Error) LogError() string {
+	if diags := e.getDiags(); diags != nil {
+		return diags.Error()
+	}
+
 	msg := AppendMsg(e.synopsis, e.label, e.message)
 
 	if e.inner != nil {
-		if innr, ok := e.inner.(*Error); ok {
-			if Equals(e, innr) {
-				innr.synopsis = "" // at least for one level, prevent duplicated synopsis
+		if inner, ok := e.inner.(*Error); ok {
+			if Equals(e, inner) {
+				inner.synopsis = "" // at least for one level, prevent duplicated synopsis
 			}
-			return AppendMsg(msg, innr.LogError())
+			return AppendMsg(msg, inner.LogError())
 		}
 		msg = AppendMsg(msg, e.inner.Error())
 	}
@@ -147,6 +153,20 @@ func (e *Error) LogError() string {
 	return msg
 }
 
+func (e *Error) getDiags() hcl.Diagnostics {
+	if e.inner != nil {
+		if diags, ok := e.inner.(hcl.Diagnostics); ok {
+			return diags
+		}
+
+		if inner, ok := e.inner.(*Error); ok {
+			return inner.getDiags()
+		}
+	}
+
+	return nil
+}
+
 // HTTPStatus returns the configured http status code this error should be served with.
 func (e *Error) HTTPStatus() int {
 	if e.httpStatus == 0 {
diff --git a/handler/transport/oauth2_req_auth.go b/handler/transport/oauth2_req_auth.go
index 158485aa..2d17eb1a 100644
--- a/handler/transport/oauth2_req_auth.go
+++ b/handler/transport/oauth2_req_auth.go
@@ -6,13 +6,23 @@ import (
 	"net/url"
 	"sync"
 
+	"github.com/hashicorp/hcl/v2"
+	"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/oauth2"
 )
 
+var supportedGrantTypes = map[string]struct{}{
+	config.ClientCredentials: struct{}{},
+	config.JwtBearer:         struct{}{},
+	config.Password:          struct{}{},
+}
+
 // OAuth2ReqAuth represents the transport <OAuth2ReqAuth> object.
 type OAuth2ReqAuth struct {
 	config       *config.OAuth2ReqAuth
@@ -24,17 +34,14 @@ 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,
+func NewOAuth2ReqAuth(evalCtx *hcl.EvalContext, conf *config.OAuth2ReqAuth, memStore *cache.MemoryStore,
 	asBackend http.RoundTripper) (RequestAuthorizer, error) {
 
-	if conf.GrantType != "client_credentials" && conf.GrantType != "password" {
+	if _, supported := supportedGrantTypes[conf.GrantType]; !supported {
 		return nil, fmt.Errorf("grant_type %s not supported", conf.GrantType)
 	}
 
-	// grant_type password undocumented feature!
-	// WARNING: this implementation is no proper password flow, but a flow with username and password to login _exactly one_ user
-	// the received access token is stored in cache just like with the client credentials flow
-	if conf.GrantType == "password" {
+	if conf.GrantType == config.Password {
 		if conf.Username == "" {
 			return nil, fmt.Errorf("username must not be empty with grant_type=password")
 		}
@@ -50,6 +57,30 @@ func NewOAuth2ReqAuth(conf *config.OAuth2ReqAuth, memStore *cache.MemoryStore,
 		}
 	}
 
+	assertionValue, err := eval.Value(evalCtx, conf.AssertionExpr)
+	if err != nil {
+		return nil, err
+	}
+
+	if conf.GrantType == config.JwtBearer {
+		if assertionValue.IsNull() && assertionValue.Type() == cty.DynamicPseudoType {
+			return nil, fmt.Errorf("missing assertion with grant_type=%s", conf.GrantType)
+		}
+	} else {
+		if !(assertionValue.IsNull() && assertionValue.Type() == cty.DynamicPseudoType) {
+			return nil, fmt.Errorf("assertion must not be set with grant_type=%s", conf.GrantType)
+		}
+	}
+
+	if conf.ClientAuthenticationRequired() {
+		if conf.ClientID == "" {
+			return nil, fmt.Errorf("client_id must not be empty")
+		}
+		if conf.ClientSecret == "" {
+			return nil, fmt.Errorf("client_secret must not be empty")
+		}
+	}
+
 	oauth2Client, err := oauth2.NewClient(conf.GrantType, conf, conf, asBackend)
 	if err != nil {
 		return nil, err
@@ -66,6 +97,24 @@ func NewOAuth2ReqAuth(conf *config.OAuth2ReqAuth, memStore *cache.MemoryStore,
 }
 
 func (oa *OAuth2ReqAuth) GetToken(req *http.Request) error {
+	requestContext := eval.ContextFromRequest(req).HCLContext()
+	assertionValue, err := eval.Value(requestContext, oa.config.AssertionExpr)
+	if err != nil {
+		return err
+	}
+
+	formParams := url.Values{}
+
+	if oa.config.GrantType == config.JwtBearer {
+		if assertionValue.IsNull() {
+			return fmt.Errorf("null assertion with grant_type=%s", oa.config.GrantType)
+		} else if assertionValue.Type() != cty.String {
+			return fmt.Errorf("assertion must evaluate to a string")
+		} else {
+			formParams.Set("assertion", assertionValue.AsString())
+		}
+	}
+
 	token := oa.readAccessToken()
 	if token != "" {
 		req.Header.Set("Authorization", "Bearer "+token)
@@ -83,11 +132,9 @@ func (oa *OAuth2ReqAuth) GetToken(req *http.Request) error {
 		return nil
 	}
 
-	formParams := url.Values{}
 	if oa.config.Scope != "" {
 		formParams.Set("scope", oa.config.Scope)
 	}
-	// password and username undocumented feature!
 	if oa.config.Password != "" || oa.config.Username != "" {
 		formParams.Set("username", oa.config.Username)
 		formParams.Set("password", oa.config.Password)
diff --git a/oauth2/client.go b/oauth2/client.go
index 295a82b6..c42db3ca 100644
--- a/oauth2/client.go
+++ b/oauth2/client.go
@@ -77,6 +77,10 @@ func (c *Client) newTokenRequest(ctx context.Context, formParams url.Values) (*h
 }
 
 func authenticateClient(clientConfig config.OAuth2Client, formParams *url.Values, tokenReq *http.Request) {
+	if !clientConfig.ClientAuthenticationRequired() {
+		return
+	}
+
 	clientID := clientConfig.GetClientID()
 	clientSecret := clientConfig.GetClientSecret()
 	if authMethod := clientConfig.GetTokenEndpointAuthMethod(); authMethod == nil || *authMethod == "client_secret_basic" {


Test patch:
diff --git a/config/configload/load_test.go b/config/configload/load_test.go
index 83bae663..3e7ed214 100644
--- a/config/configload/load_test.go
+++ b/config/configload/load_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/avenga/couper/cache"
 	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/internal/test"
 )
 
@@ -85,12 +86,12 @@ func TestHealthCheck(t *testing.T) {
 		{
 			"Bad interval",
 			`interval = "10sec"`,
-			`time: unknown unit "sec" in duration "10sec"`,
+			`configuration error: foo: time: unknown unit "sec" in duration "10sec"`,
 		},
 		{
 			"Bad timeout",
 			`timeout = 1`,
-			`time: missing unit in duration "1"`,
+			`configuration error: foo: time: missing unit in duration "1"`,
 		},
 		{
 			"Bad threshold",
@@ -147,7 +148,11 @@ func TestHealthCheck(t *testing.T) {
 
 			var errorMsg = ""
 			if err != nil {
-				errorMsg = err.Error()
+				if gErr, ok := err.(errors.GoError); ok {
+					errorMsg = gErr.LogError()
+				} else {
+					errorMsg = err.Error()
+				}
 			}
 
 			if tt.error != errorMsg {
diff --git a/main_test.go b/main_test.go
index cb5b483c..3d50ee25 100644
--- a/main_test.go
+++ b/main_test.go
@@ -51,7 +51,7 @@ func Test_realmain(t *testing.T) {
 		{"empty string in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/13_couper.hcl"}, nil, `level=error msg="%s/13_couper.hcl:3,5-27: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="%s/14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
 		{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="%s/15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
-		{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="anonymous backend (\"anonymous_3_11\") cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
+		{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="configuration error: anonymous_3_11: anonymous backend (\"anonymous_3_11\") cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 2a0d5a75..66a66085 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -4233,10 +4233,10 @@ func TestFunction_to_number_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"string", "/v1/to_number/string", `expression evaluation error: ` + wd + `/01_couper.hcl:62,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`},
-		{"bool", "/v1/to_number/bool", `expression evaluation error: ` + wd + `/01_couper.hcl:70,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`},
-		{"tuple", "/v1/to_number/tuple", `expression evaluation error: ` + wd + `/01_couper.hcl:78,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`},
-		{"object", "/v1/to_number/object", `expression evaluation error: ` + wd + `/01_couper.hcl:86,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`},
+		{"string", "/v1/to_number/string", wd + `/01_couper.hcl:62,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`},
+		{"bool", "/v1/to_number/bool", wd + `/01_couper.hcl:70,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`},
+		{"tuple", "/v1/to_number/tuple", wd + `/01_couper.hcl:78,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`},
+		{"object", "/v1/to_number/object", wd + `/01_couper.hcl:86,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -4277,9 +4277,9 @@ func TestFunction_length_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"object", "/v1/length/object", `expression evaluation error: ` + wd + `/01_couper.hcl:123,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
-		{"string", "/v1/length/string", `expression evaluation error: ` + wd + `/01_couper.hcl:131,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
-		{"null", "/v1/length/null", `expression evaluation error: ` + wd + `/01_couper.hcl:139,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`},
+		{"object", "/v1/length/object", wd + `/01_couper.hcl:123,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
+		{"string", "/v1/length/string", wd + `/01_couper.hcl:131,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
+		{"null", "/v1/length/null", wd + `/01_couper.hcl:139,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -4320,7 +4320,7 @@ func TestFunction_lookup_errors(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"null inputMap", "/v1/lookup/inputMap-null", `expression evaluation error: ` + wd + `/01_couper.hcl:200,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`},
+		{"null inputMap", "/v1/lookup/inputMap-null", wd + `/01_couper.hcl:200,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -5298,6 +5298,12 @@ func TestEndpoint_ResponseNilEvaluation(t *testing.T) {
 func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
 	client := newClient()
 
+	wd, werr := os.Getwd()
+	if werr != nil {
+		t.Fatal(werr)
+	}
+	wd = wd + "/testdata/integration/endpoint_eval"
+
 	shutdown, hook := newCouper("testdata/integration/endpoint_eval/20_couper.hcl", test.New(t))
 	defer shutdown()
 
@@ -5307,13 +5313,13 @@ func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
 	}
 
 	for _, tc := range []testCase{
-		{"/conditional/null", "20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."},
-		{"/conditional/string", "20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/number", "20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/tuple", "20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/object", "20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/string/expr", "20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."},
-		{"/conditional/number/expr", "20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/null", wd + "/20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."},
+		{"/conditional/string", wd + "/20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/number", wd + "/20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/tuple", wd + "/20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/object", wd + "/20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/string/expr", wd + "/20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."},
+		{"/conditional/number/expr", wd + "/20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
@@ -5343,10 +5349,7 @@ func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
 			time.Sleep(time.Millisecond * 100)
 			entry := hook.LastEntry()
 			if entry != nil && entry.Level == logrus.ErrorLevel {
-				if !strings.HasPrefix(entry.Message, "expression evaluation error: ") {
-					subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message)
-				}
-				if !strings.HasSuffix(entry.Message, tc.expMessage) {
+				if entry.Message != tc.expMessage {
 					subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message)
 				}
 			}
diff --git a/server/http_oauth2_test.go b/server/http_oauth2_test.go
index fafb6aac..47615f9b 100644
--- a/server/http_oauth2_test.go
+++ b/server/http_oauth2_test.go
@@ -2,6 +2,7 @@ package server_test
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -19,6 +20,10 @@ import (
 	"github.com/sirupsen/logrus"
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 
+	"github.com/avenga/couper/cache"
+	"github.com/avenga/couper/config/configload"
+	"github.com/avenga/couper/config/runtime"
+	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval/lib"
 	"github.com/avenga/couper/internal/test"
 	"github.com/avenga/couper/logging"
@@ -220,6 +225,11 @@ func TestEndpoints_OAuth2_Options(t *testing.T) {
 			`client_id=my_client&client_secret=my_client_secret&grant_type=password&password=pass&scope=scope1+scope2&username=user`,
 			"",
 		},
+		{
+			"16_couper.hcl",
+			`assertion=GET&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer`,
+			"",
+		},
 	} {
 		var tokenSeenCh chan struct{}
 
@@ -271,6 +281,253 @@ func TestEndpoints_OAuth2_Options(t *testing.T) {
 	}
 }
 
+func TestOAuth2_Config_Errors(t *testing.T) {
+	log, _ := test.NewLogger()
+
+	type testCase struct {
+		name  string
+		hcl   string
+		error string
+	}
+
+	for _, tc := range []testCase{
+		{
+			"grant_type client_credentials without client_id",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_secret  = "my_client_secret"
+      grant_type     = "client_credentials"
+    }
+  }
+}
+`,
+			"configuration error: be: client_id must not be empty",
+		},
+		{
+			"grant_type client_credentials without client_secret",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      grant_type     = "client_credentials"
+    }
+  }
+}
+`,
+			"configuration error: be: client_secret must not be empty",
+		},
+		{
+			"grant_type password without client_id",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_secret  = "my_client_secret"
+      grant_type     = "password"
+      username       = "my_user"
+      password       = "my_password"
+    }
+  }
+}
+`,
+			"configuration error: be: client_id must not be empty",
+		},
+		{
+			"grant_type password without client_secret",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      grant_type     = "password"
+      username       = "my_user"
+      password       = "my_password"
+    }
+  }
+}
+`,
+			"configuration error: be: client_secret must not be empty",
+		},
+		{
+			"username with grant_type client_credentials",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "client_credentials"
+      username       = "my_user"
+    }
+  }
+}
+`,
+			"configuration error: be: username must not be set with grant_type=client_credentials",
+		},
+		{
+			"password with grant_type client_credentials",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "client_credentials"
+      password       = "my_password"
+    }
+  }
+}
+`,
+			"configuration error: be: password must not be set with grant_type=client_credentials",
+		},
+		{
+			"username with grant_type jwt-bearer",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      grant_type     = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+      username       = "my_user"
+    }
+  }
+}
+`,
+			"configuration error: be: username must not be set with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer",
+		},
+		{
+			"password with grant_type jwt-bearer",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      grant_type     = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+      password       = "my_password"
+    }
+  }
+}
+`,
+			"configuration error: be: password must not be set with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer",
+		},
+		{
+			"assertion with grant_type client_credentials",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "client_credentials"
+      assertion      = "my_assertion"
+    }
+  }
+}
+`,
+			"configuration error: be: assertion must not be set with grant_type=client_credentials",
+		},
+		{
+			"assertion with grant_type password",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "password"
+      username       = "my_user"
+      password       = "my_password"
+      assertion      = "my_assertion"
+    }
+  }
+}
+`,
+			"configuration error: be: assertion must not be set with grant_type=password",
+		},
+		{
+			"missing username with grant_type password",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "password"
+    }
+  }
+}
+`,
+			"configuration error: be: username must not be empty with grant_type=password",
+		},
+		{
+			"missing password with grant_type password",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "password"
+      username       = "my_user"
+    }
+  }
+}
+`,
+			"configuration error: be: password must not be empty with grant_type=password",
+		},
+		{
+			"missing assertion with grant_type jwt-bearer",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      grant_type     = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+    }
+  }
+}
+`,
+			"configuration error: be: missing assertion with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer",
+		},
+	} {
+		var errMsg string
+		conf, err := configload.LoadBytes([]byte(tc.hcl), "couper.hcl")
+		if conf != nil {
+			logger := log.WithContext(context.TODO())
+
+			tmpStoreCh := make(chan struct{})
+			defer close(tmpStoreCh)
+
+			_, err = runtime.NewServerConfiguration(conf, logger, cache.New(logger, tmpStoreCh))
+		}
+
+		if err != nil {
+			if _, ok := err.(errors.GoError); ok {
+				errMsg = err.(errors.GoError).LogError()
+			} else {
+				errMsg = err.Error()
+			}
+		}
+
+		if !strings.HasPrefix(errMsg, tc.error) {
+			t.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot:  %q", tc.name, tc.error, errMsg)
+		}
+	}
+}
+
 func TestOAuth2_AccessControl(t *testing.T) {
 	client := newClient()
 
diff --git a/server/testdata/oauth2/16_couper.hcl b/server/testdata/oauth2/16_couper.hcl
new file mode 100644
index 00000000..0635fa38
--- /dev/null
+++ b/server/testdata/oauth2/16_couper.hcl
@@ -0,0 +1,17 @@
+server {
+  api {
+    endpoint "/" {
+      proxy {
+        url = "https://example.com/"
+
+        backend {
+          oauth2 {
+            token_endpoint = "{{.asOrigin}}/options"
+            grant_type     = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+            assertion      = request.method # easier for test purpose, should of course be a signed JWT
+          }
+        }
+      }
+    }
+  }
+}