Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New grant types #555

Merged
merged 13 commits into from
Aug 16, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
4 changes: 4 additions & 0 deletions config/ac_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions config/ac_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 8 additions & 3 deletions config/configload/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions config/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions config/oauth2ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 3 additions & 2 deletions config/runtime/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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,
Expand Down
31 changes: 18 additions & 13 deletions docs/website/content/2.configuration/4.block/oauth2req_auth.md
Original file line number Diff line number Diff line change
@@ -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).
28 changes: 24 additions & 4 deletions errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net/http"
"strings"

"github.com/hashicorp/hcl/v2"
)

type Error struct {
Expand Down Expand Up @@ -132,21 +134,39 @@ 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())
}

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 {
Expand Down
Loading