Skip to content

Commit

Permalink
feat: New oauth2_client_credentials finalizer (#959)
Browse files Browse the repository at this point in the history
Co-authored-by: nett_hier <[email protected]>
  • Loading branch information
dadrus and netthier authored Oct 12, 2023
1 parent 64dc7a7 commit 4c9f807
Show file tree
Hide file tree
Showing 12 changed files with 1,680 additions and 15 deletions.
13 changes: 13 additions & 0 deletions docs/content/docs/configuration/reference/reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,19 @@ rules:
config:
cookies:
foo-bar: '{{ .Subject.ID }}'
- id: get_token
type: oauth2_client_credentials
config:
header:
name: X-Token
token_url: https://my-oauth-provider.com/token
client_id: my_client
client_secret: VerySecret!
auth_method: basic_auth
cache_ttl: 5m
scopes:
- foo
- bar
error_handlers:
- id: default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ menu:
parent: "Pipeline Mechanisms"
---

Finalizers, as the name implies, finalize the successful execution of the pipeline and make the available information about the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] and the link:{{< relref "overview.adoc#_request" >}}[`Request`] to the upstream service in a format expected, respectively required by it. This ranges from adding a simple header to a structured JWT in a specific header.
Finalizers, as the name implies, finalize the execution of the pipeline and enrich the request with data such as subject information or authentication tokens required by the upstream service. The available options range from adding a simple header over a structured JWT in a specific header, to driving specific protocols, e.g. to obtain a token required by the upstream service.

== Finalizer Types

Expand Down Expand Up @@ -83,9 +83,11 @@ config:

=== JWT

This finalizer enables transformation of the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] and the link:{{< relref "overview.adoc#_request" >}}[`Request`] object into a token in a https://www.rfc-editor.org/rfc/rfc7519[JWT] format, which is then made available to your upstream service in either the HTTP `Authorization` header with `Bearer` scheme set, or in a custom header. In addition to setting the JWT specific claims, it allows setting custom claims as well. Your upstream service can then verify the signature of the JWT by making use of heimdall's JWKS endpoint to retrieve the required public keys/certificates from.
This finalizer enables transformation of the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] object into a token in a https://www.rfc-editor.org/rfc/rfc7519[JWT] format, which is then made available to your upstream service in either the HTTP `Authorization` header with `Bearer` scheme set, or in a custom header. In addition to setting the JWT specific claims, it allows setting custom claims as well. Your upstream service can then verify the signature of the JWT by making use of heimdall's JWKS endpoint to retrieve the required public keys/certificates from.

NOTE: To enable the usage of this finalizer, you have to set the `type` property to `jwt`. The usage of this finalizer type requires a configured link:{{< relref "/docs/configuration/cryptographic_material.adoc" >}}[Signer] as well. At least it is a must in production environments.
To enable the usage of this finalizer, you have to set the `type` property to `jwt`.

NOTE: The usage of this finalizer type requires a configured link:{{< relref "/docs/configuration/cryptographic_material.adoc" >}}[Signer] as well. At least it is a must in production environments.

Configuration using the `config` property is optional. Following properties are available:

Expand Down Expand Up @@ -122,3 +124,66 @@ config:
}
----
====

=== OAuth2 Client Credentials

This finalizer drives the https://www.rfc-editor.org/rfc/rfc6749#section-4.4[OAuth2 Client Credentials Grant] flow to obtain a token, which should be used for communication with the upstream service. By default, as long as not otherwise configured (see the options below), the obtained token is made available to your upstream service in the HTTP `Authorization` header with `Bearer` scheme set. Unlike the other finalizers, it does not have access to any objects created by the rule execution pipeline.

To enable the usage of this finalizer, you have to set the `type` property to `oauth2_client_credentials`.

Configuration using the `config` property is mandatory. Following properties are available:

* *`token_url`*: _string_ (mandatory, not overridable)
+
The token endpoint of the authorization server.

* *`client_id`*: _string_ (mandatory, not overridable)
+
The client identifier for heimdall.

* *`client_secret`*: _string_ (mandatory, not overridable)
+
The client secret for heimdall.

* *`auth_method`*: _string_ (optional, not overridable)
+
The authentication method to be used according to https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1[RFC 6749, Client Password]. Can one of

** `basic_auth` (default if `auth_method` is not set): With that authentication method, the `"application/x-www-form-urlencoded"` encoded values of `client_id` and `client_secret` are sent to the authorization server via the `Authorization` header using the `Basic` scheme.

** `request_body`: With that authentication method the `client_id` and `client_secret` are sent in the request body together with the other parameters (e.g. `scopes`) defined by the flow.
+
WARNING: Usage of `request_body` authentication method is not recommended and should be avoided.

* *`scopes`*: _string array_ (optional, overridable)
+
The scopes required for the access token.

* *`cache_ttl`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_duration" >}}[Duration]_ (optional, overridable)
+
How long to cache the token received from the token endpoint. Defaults to the token expiration information from the token endpoint (the value of the `expires_in` field) if present. If the token expiration inforation is not present and `cache_ttl` is not configured, the received token is not cached. If the token expiration information is present in the response and `cache_ttl` is configured the shorter value is taken. If caching is enabled, the token is cached until 5 seconds before its expiration. To disable caching, set it to `0s`. The cache key calculation is based on the entire `oauth2_client_credentials` configuration without considering the `header` property.

* *`header`*: _object_ (optional, overridable)
+
Defines the `name` and `scheme` to be used for the header. Defaults to `Authorization` with scheme `Bearer`. If defined, the `name` property must be set. If `scheme` is not defined, no scheme will be prepended to the resulting JWT.

.OAuth2 Client Credentials finalizer configuration
====
[source, yaml]
----
id: get_token
type: oauth2_client_credentials
config:
cache_ttl: 5m
header:
name: X-Token
scheme: MyScheme
token_url: https://my-oauth-provider.com/token
client_id: my_client
client_secret: VerySecret!
auth_method: basic_auth
scopes:
- foo
- bar
----
====
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ All mechanisms supported by heimdall fall into following categories:
* link:{{< relref "authenticators.adoc">}}[Authenticators], which inspect HTTP requests for presence of authentication objects, like e.g. the presence of a specific cookie. If such objects exist, authenticators verify the related authentication status and obtain information about the corresponding subject. A subject, could be a user who tries to use particular functionality of the upstream service, a machine (if you have machine-2-machine interaction), or something different. Authenticators ensure the subject is authenticated and the information available about it is valid.
* link:{{< relref "authorizers.adoc">}}[Authorizers], which ensure that the subject obtained via an authenticator has the required permissions to submit the given HTTP request and thus to execute the corresponding logic in the upstream service. E.g. a specific endpoint of the upstream service might only be accessible to a "user" from the "admin" group, or to an HTTP request if a specific HTTP header is set.
* link:{{< relref "contextualizers.adoc">}}[Contextualizers], which enrich the information about the subject obtained via an authenticator with further contextual information, required either by the upstream service itself or an authorizer. This can be handy if the actual authentication system doesn't have all information about the subject (which is usually the case in microservice architectures), or if dynamic information about the subject, like the current location based on the IP address, is required.
* link:{{< relref "finalizers.adoc">}}[Finalizers], which, as the name implies, finalize the successful execution of the pipeline and make the gathered information about the subject and the request available to the upstream service in a format expected, respectively required by it. This ranges from adding a simple header or cookie, to a structured JWT.
* link:{{< relref "finalizers.adoc">}}[Finalizers], which, as the name implies, finalize the execution of the pipeline and enrich the request with data such as subject information or authentication tokens required by the upstream service. The available options range from adding a simple header over a structured JWT, to driving specific protocols, e.g. to obtain a token required by the upstream service.
* link:{{< relref "error_handlers.adoc">}}[Error Handlers], which are responsible for execution of logic if any of the mechanisms described above fail. These range from a simple error response to the client, which sent the request, to sophisticated ones, supporting complex logic and redirects.

== General Configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/getting_started/concepts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Here, heimdall communicates with other systems as well, either to get further in
Here, heimdall performs authorization checks, either locally, or by communicating with yet again further systems, like Open Policy Agent, Ory Keto and alike.
** *finalization* mechanisms, so-called link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/finalizers.adoc" >}}[Finalizers], to be executed (if multiple are defined, they are executed in the order of their definition) - step 4 in the figure above.
+
This step finalizes the execution of the pipeline and transform the information collected so far about the subject and the request into objects expected by the upstream service. That reaches from a simple custom header, carrying e.g. the id of the subject, to a JWT carried in the `Authorization` header.
This step finalizes the execution of the pipeline and enriches the request with data such as subject information or authentication tokens required by the upstream service. The available options range from adding a simple header over a structured JWT, to driving specific protocols, e.g. to obtain a token required by the upstream service.
* an error pipeline, consisting of link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/error_handlers.adoc" >}}[error handler] mechanisms (if multiple are defined, they are executed as fallbacks), which are executed if any of the regular pipeline mechanisms fail. These mechanisms range from a simple error response to the client (which sent the request), to sophisticated ones supporting complex logic and redirects.

The diagram below sketches the related execution logic
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,6 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-co-op/gocron v1.35.1 h1:xi0tfAhxeAmGUKkjiA7bTIjh2VdBJpUYDJ+lPx/EPcM=
github.com/go-co-op/gocron v1.35.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no=
github.com/go-co-op/gocron v1.35.2 h1:lG3rdA9TqBBC/PtT2ukQqgLm6jEepnAzz3+OQetvPTE=
github.com/go-co-op/gocron v1.35.2/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no=
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 h1:zga7zaRE8HCbWjcXMDlfvmQtH0/kMVLo7cQ48dy6kWg=
Expand Down Expand Up @@ -644,8 +642,6 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
Expand Down
14 changes: 14 additions & 0 deletions internal/config/test_data/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,20 @@ rules:
config:
cookies:
foo-bar: '{{ .Subject.ID }}'
- id: client_cred_grant
type: oauth2_client_credentials
config:
token_url: https://my-auth-provider/token
client_id: foo
client_secret: bar
auth_method: basic_auth
cache_ttl: 5m
scopes:
- foo
- bar
header:
name: My-Header
scheme: Foo
error_handlers:
- id: default
type: default
Expand Down
13 changes: 12 additions & 1 deletion internal/rules/endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,14 @@ func (e Endpoint) CreateRequest(ctx context.Context, body io.Reader, rndr Render
return req, nil
}

func (e Endpoint) SendRequest(ctx context.Context, body io.Reader, renderer Renderer) ([]byte, error) {
type ResponseReader func(resp *http.Response) ([]byte, error)

func (e Endpoint) SendRequest(
ctx context.Context,
body io.Reader,
renderer Renderer,
reader ...ResponseReader,
) ([]byte, error) {
req, err := e.CreateRequest(ctx, body, renderer)
if err != nil {
return nil, err
Expand All @@ -143,6 +150,10 @@ func (e Endpoint) SendRequest(ctx context.Context, body io.Reader, renderer Rend

defer resp.Body.Close()

if len(reader) != 0 {
return reader[0](resp)
}

return e.readResponse(resp)
}

Expand Down
9 changes: 5 additions & 4 deletions internal/rules/mechanisms/finalizers/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
package finalizers

const (
FinalizerNoop = "noop"
FinalizerJwt = "jwt"
FinalizerHeader = "header"
FinalizerCookie = "cookie"
FinalizerNoop = "noop"
FinalizerJwt = "jwt"
FinalizerHeader = "header"
FinalizerCookie = "cookie"
FinalizerOAuth2ClientCredentials = "oauth2_client_credentials" // nolint: gosec
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestCreateFinalizerPrototype(t *testing.T) {
t.Parallel()

// there are 4 finalizers implemented, which should have been registered
require.Len(t, typeFactories, 4)
require.Len(t, typeFactories, 5)

for _, tc := range []struct {
uc string
Expand Down
Loading

0 comments on commit 4c9f807

Please sign in to comment.