Skip to content

Commit

Permalink
feat: jwt finalizer extended to support templating via values pro…
Browse files Browse the repository at this point in the history
…perty (#2193)
  • Loading branch information
dadrus authored Feb 18, 2025
1 parent 3ba8c7b commit bf833c4
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 115 deletions.
38 changes: 29 additions & 9 deletions docs/content/docs/mechanisms/finalizers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,29 +82,33 @@ config:

== JWT

This finalizer enables transformation of the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_subject" >}}[`Subject`] and the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_outputs" >}}[`Outputs`] objects as custom claims 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. 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 "/docs/mechanisms/evaluation_objects.adoc#_subject" >}}[`Subject`] and the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_outputs" >}}[`Outputs`] objects into custom claims within a https://www.rfc-editor.org/rfc/rfc7519[JWT]. The resulting token is then made available to your upstream service in either the HTTP `Authorization` header (using the `Bearer` scheme) or in a custom header. Your upstream service can verify the JWT's signature using heimdall's JWKS endpoint to retrieve the necessary public keys/certificates.

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

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

* *`signer`*: _link:{{< relref "/docs/configuration/types.adoc#_signer" >}}[Signer]_ (mandatory, not overridable)
+
The configuration of the key material used for signature creation purposes, as well as the name used for the `iss` claim.
Defines the key material for signing the JWT, as well as the `iss` claim.

* *`claims`*: _string_ (optional, overridable)
+
Your template with custom claims, you would like to add to the JWT (See also link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_templating" >}}[Templating]).
A link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_templating" >}}[template] specifying custom claims for the JWT. The template can use link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_values" >}}[`Values`], link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_outputs" >}}[`Outputs`], and link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_subject" >}}[`Subject`] objects.

* *`values`*: _map of strings_ (optional, overridable)
+
A key-value map accessible as link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_values" >}}[`Values`] in the template engine for rendering claims. Values in this map can also be templated with access to link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_subject" >}}[`Subject`] and link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_outputs" >}}[`Outputs`].

* *`ttl`*: _link:{{< relref "/docs/configuration/types.adoc#_duration" >}}[Duration]_ (optional, overridable)
+
Defines how long the JWT should be valid. Defaults to 5 minutes. Heimdall sets the `iat` and the `nbf` claims to the current system time. The value of the `exp` claim is then influenced by the `ttl` property.
Defines the JWT's validity period. Defaults to 5 minutes. Heimdall automatically sets the `iat` and `nbf` claims to the current system time, and `exp` is calculated based on the `ttl` value.

* *`header`*: _object_ (optional, not 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.
Specifies the HTTP header `name` and optional `scheme` for passing the JWT. Defaults to `Authorization` with scheme `Bearer`. If defined, `name` is required, and if `scheme` is omitted, the JWT is set as a raw value.

The generated JWT is always cached until 5 seconds before its expiration. The cache key is calculated from the entire configuration of the finalizer instance and the available information about the current subject.
The generated JWT is cached until 5 seconds before expiration. The cache key is computed based on the finalizer's configuration, the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_subject" >}}[`Subject`], and the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_outputs" >}}[`Outputs`] attributes.

.JWT finalizer configuration
====
Expand All @@ -121,9 +125,25 @@ config:
{{ $user_name := .Subject.Attributes.identity.user_name -}}
"email": {{ quote .Subject.Attributes.identity.email }},
"email_verified": {{ .Subject.Attributes.identity.email_verified }},
"name": {{ if $user_name }}{{ quote $user_name }}{{ else }}{{ quote $email }}{{ end }}
"name": {{ if $user_name }}{{ quote $user_name }}{{ else }}{{ quote $email }}{{ end }},
"extra": {{ .Values | toJson }}
}
----
In a rule that references the `jwt_finalizer`, additional claims can be dynamically inserted into the `"extra"` claim without redefining `claims`:
[source,yaml]
----
- id: some_rule
# Other rule properties
execute:
- # Other mechanisms
- finalizer: jwt_finalizer
config:
values:
foo: bar
user_id: '{{ .Subject.ID }}'
- # Other mechanisms
----
====

== OAuth2 Client Credentials
Expand Down
33 changes: 28 additions & 5 deletions internal/rules/mechanisms/finalizers/jwt_finalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/dadrus/heimdall/internal/heimdall"
"github.com/dadrus/heimdall/internal/rules/mechanisms/subject"
"github.com/dadrus/heimdall/internal/rules/mechanisms/template"
"github.com/dadrus/heimdall/internal/rules/mechanisms/values"
"github.com/dadrus/heimdall/internal/x"
"github.com/dadrus/heimdall/internal/x/errorchain"
"github.com/dadrus/heimdall/internal/x/stringx"
Expand Down Expand Up @@ -66,6 +67,7 @@ type jwtFinalizer struct {
headerName string
headerScheme string
signer *jwtSigner
v values.Values
}

func newJWTFinalizer(app app.Context, id string, rawConfig map[string]any) (*jwtFinalizer, error) {
Expand All @@ -81,6 +83,7 @@ func newJWTFinalizer(app app.Context, id string, rawConfig map[string]any) (*jwt
Signer SignerConfig `mapstructure:"signer" validate:"required"`
TTL *time.Duration `mapstructure:"ttl" validate:"omitempty,gt=1s"`
Claims template.Template `mapstructure:"claims"`
Values values.Values `mapstructure:"values"`
Header *HeaderConfig `mapstructure:"header"`
}

Expand Down Expand Up @@ -111,6 +114,7 @@ func newJWTFinalizer(app app.Context, id string, rawConfig map[string]any) (*jwt
func() string { return conf.Header.Scheme },
func() string { return "Bearer" }),
signer: signer,
v: conf.Values,
}

app.CertificateObserver().Add(fin)
Expand Down Expand Up @@ -168,6 +172,7 @@ func (f *jwtFinalizer) WithConfig(rawConfig map[string]any) (Finalizer, error) {
type Config struct {
TTL *time.Duration `mapstructure:"ttl" validate:"omitempty,gt=1s"`
Claims template.Template `mapstructure:"claims"`
Values values.Values `mapstructure:"values"`
}

var conf Config
Expand All @@ -186,6 +191,7 @@ func (f *jwtFinalizer) WithConfig(rawConfig map[string]any) (Finalizer, error) {
headerName: f.headerName,
headerScheme: f.headerScheme,
signer: f.signer,
v: f.v.Merge(conf.Values),
}, nil
}

Expand All @@ -197,31 +203,43 @@ func (f *jwtFinalizer) generateToken(ctx heimdall.RequestContext, sub *subject.S
logger := zerolog.Ctx(ctx.Context())
logger.Debug().Msg("Generating new JWT")

claims := map[string]any{}
result := map[string]any{}

if f.claims != nil {
vals, err := f.claims.Render(map[string]any{
vals, err := f.v.Render(map[string]any{
"Subject": sub,
"Outputs": ctx.Outputs(),
})
if err != nil {
return "", errorchain.NewWithMessage(heimdall.ErrInternal,
"failed to render values").
WithErrorContext(f).
CausedBy(err)
}

claims, err := f.claims.Render(map[string]any{
"Subject": sub,
"Outputs": ctx.Outputs(),
"Values": vals,
})
if err != nil {
return "", errorchain.
NewWithMessage(heimdall.ErrInternal, "failed to render claims").
WithErrorContext(f).
CausedBy(err)
}

logger.Debug().Str("_value", vals).Msg("Rendered template")
logger.Debug().Str("_value", claims).Msg("Rendered template")

if err = json.Unmarshal(stringx.ToBytes(vals), &claims); err != nil {
if err = json.Unmarshal(stringx.ToBytes(claims), &result); err != nil {
return "", errorchain.
NewWithMessage(heimdall.ErrInternal, "failed to unmarshal claims rendered by template").
WithErrorContext(f).
CausedBy(err)
}
}

token, err := f.signer.Sign(sub.ID, f.ttl, claims)
token, err := f.signer.Sign(sub.ID, f.ttl, result)
if err != nil {
return "", errorchain.
NewWithMessage(heimdall.ErrInternal, "failed to sign token").
Expand Down Expand Up @@ -249,6 +267,11 @@ func (f *jwtFinalizer) calculateCacheKey(ctx heimdall.RequestContext, sub *subje
hash.Write(ttlBytes)
hash.Write(sub.Hash())

for key, val := range f.v {
hash.Write(stringx.ToBytes(key))
hash.Write(val.Hash())
}

rawSub, _ := json.Marshal(ctx.Outputs())
hash.Write(rawSub)

Expand Down
Loading

0 comments on commit bf833c4

Please sign in to comment.