mechanisms:
authenticators:
<list of authenticators>
authorizers:
<list of authorizers>
contextualizers:
<list of contextualizers>
finalizers:
<list of finalizers>
error_handlers:
<list of error handlers>
Mechanisms Overview
All mechanisms supported by heimdall fall into following categories:
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.
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.
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.
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 doing nothing, adding a simple header over a structured JWT, to driving specific protocols, e.g. to obtain a token required by the upstream service.
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
All of the above said mechanisms must be configured in the mechanisms
section of heimdall’s configuration. Only those mechanisms, which have been configured, can then be (re-)used by rules.
Each mechanism configuration entry must contain at least the following properties:
id
- The unique identifier of the mechanism. Identifiers are used to reference the required mechanism within a rule. You can choose whatever identifier, you want. It is just a name. It must however be unique across all defined mechanisms of a particular mechanism category (like authenticator, authorizer, etc.).type
- The specific type of the mechanism.
Depending on the mechanism type, there can be an additional config
property, which then holds the mechanism’s specific configuration and an if
property, which specifies when a particular mechanism should be executed.
Every mechanism type can be configured as many times as needed. However, for those, which don’t have a configuration, it doesn’t really make sense, as all of them would behave the same way.
For example, your authenticator definitions could look like this:
mechanisms:
authenticators:
- id: foo
type: bar
- id: baz
type: bla
config:
bla: bar
- id: zab
type: bar
- id: oof
type: bla
config:
bar: bla
The above snippet configures two instances of an imaginary authenticator of a type bar
, made available for usage in rules via ids foo
and zab
, as well as two instances of an imaginary authenticator of a type bla
, made available for usage in rule via ids baz
and oof
. The baz
and oof
authenticators are different, as they are configured differently, but foo
and zab
authenticators do not have any configuration. So, they behave the same way and there is actually no need to define two instances of them.
In simplest case a rule will just reuse mechanisms in its pipeline. In more complex cases a rule can reconfigure parts of it (More about rules configuration can be found here). Which parts can be reconfigured, respectively overridden are mechanism specific and described in the mechanism specific documentation. Reconfiguration is always limited to the particular rule pipeline and does not affect other rules.
Here is an example which configures a couple of mechanisms:
mechanisms:
authenticators:
- id: anon_authn
type: anonymous
- id: opaque_auth_token_authn
type: oauth2_introspection
config:
introspection_endpoint:
url: http://hydra:4445/oauth2/introspect
assertions:
issuers:
- http://127.0.0.1:4444/
authorizers:
- id: deny_all_authz
type: deny
- id: local_authz
type: cel
config:
expressions:
- expression:
"manager" in Subject.Attributes.groups
message: user is not in the expected group
contextualizers:
- id: group_manager
type: generic
config:
endpoint:
url: http://group-manager.local/groups
method: GET
forward_headers:
- Authorization
cache_ttl: 1m
finalizers:
- id: jwt_finalizer
type: jwt
config:
ttl: 5m
claims: |
{
{{ $user_name := .Subject.Attributes.identity.user_name -}}
"email": {{ quote .Subject.Attributes.identity.email }},
"email_verified": {{ .Subject.Attributes.identity.email_verified }},
{{ if $user_name -}}
"name": {{ quote $user_name }}
{{ else -}}
"name": {{ quote $email }}
{{ end -}}
}
error_handlers:
- id: default
type: default
- id: authenticate_with_kratos
if: |
type(Error) in [authentication_error, authorization_error] &&
Request.Header("Accept").contains("text/html")
type: redirect
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}
Evaluation Objects
Some mechanisms support, respectively require access to different types of objects they work on, e.g. to render a header with specific values, or to check whether some expectations apply. Following objects are available and have the following structure:
Subject
This object contains the information about the authenticated subject and has the following attributes:
ID
: stringThe identifier of the subject. This value is set by the authenticator, which was able to authenticate the subject.
Attributes
: mapContains all attributes, which are known about the subject. The content is initially set by the authenticator, which was able to authenticate the subject. Mechanisms following the authenticator in a rule pipeline can update it, but cannot override any entries.
Each object of this type can be thought as a JSON object. Here some examples:
Subject = {
ID: "anonymous",
Attributes: {}
}
Subject = {
ID: "foobar",
Attributes: {
"sub": "foobar",
"exp": "1670600805",
"jti": "7b91ed8a-0251-4e02-8d51-9792785851e8",
"iat": "1670600305",
"iss": "http://testauthserver.local",
"nbf": "1670600305",
"extra": {
"foo": ["bar", "baz"]
}
}
}
Request
This object contains information about the request handled by heimdall and has the following attributes and methods:
Method
: stringThe HTTP method used, like
GET
,POST
, etc.URL
: URLThe URL of the matched request. This object has the following properties and methods:
Scheme
: stringThe HTTP scheme part of the url
Host
: stringThe host part of the url
Path
: stringThe path part of the url
RawQuery
: stringThe raw query part of the url.
String()
: methodThis method returns the URL as valid URL string of a form
scheme:host/path?query
.Query()
: methodThe parsed query with each key-value pair being a string to array of strings mapping.
ClientIPAddresses
: string arrayThe list of IP addresses the request passed through with the first entry being the ultimate client of the request. Only available if heimdall is configured to trust the client, sending this information, e.g. in the
X-Forwarded-From
header (see e.g. Decision Service trusted_proxies configuration for more details).Header(name)
: method,This method expects the name of a header as input and returns its value as a
string
. If the header is not present in the HTTP request an empty string (""
) is returned. If a header appears multiple times in the request, the returnedstring
is a comma separated list of all values.A single header may be a comma separated list of actual values as well. Best example is the Accept
header, which might be set to e.g.text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
).Cookie(name)
: method,This method expects the name of a cookie as input and returns the value of it as
string
. If the cookie is not present in the HTTP request an empty string (""
) is returned.Body()
: method,The parsed body with contents depending on the
Content-Type
header. Supported content types are any MIME types withjson
oryaml
subtype, as well asapplication/x-www-form-urlencoded
. If MIME type is unsupported, the method returns a string with the actual body contents.The actual request body is parsed only on the first use of this function. All subsequent calls return the cached result. Example 3. Example resultsIf the
Content-Type
header is set toapplication/json
and the actual request body is a valid JSON object, shown below{ "context": "heimdall" }
The call to the
Body()
function will return exactly this representation as a map.If the
Content-Type
header is set toapplication/yaml
and the actual request body is a valid YAML object, shown belowcontext: heimdall
The call to the
Body()
function will return{ "context": "heimdall" }
representation as a map.If the
Content-Type
header is set toapplication/x-www-form-urlencoded
and the actual request body is a valid object, shown belowcontext=heimdall
The call to the
Body()
function will return this representation as a map with each value being a string array. In this particular case as{ "context": [ "heimdall" ] }
.
Here is an example for a request object:
Request = {
Method: "GET",
Url: {
Scheme: "https",
Host: "localhost",
Path: "/test",
RawQuery: "baz=zab&baz=bar&foo=bar"
},
ClientIP: ["127.0.0.1", "10.10.10.10"]
}
Payload
This object represents the contents of a payload, like the request body or a response body. The contents depend on the MIME-Type of the payload. For json
, yaml
or x-www-form-urlencoded
encoded payload, the object is transformed to a JSON object. Otherwise, it is just a string.
Here some examples:
The following JSON object is a typical response from OPA.
Payload = { "result": true }
Payload = "SomeStringValue"
Error
This object represents an error, which has been raised during the execution of the pipeline and is available in if
CEL expressions of Error Handlers. Following properties are available:
Source
: stringThe ID of the mechanism, which raised the error.
Proper error handling requires attention to the actual error type available via type(Error)
.
Values
This object represents a key value map, with both, the key and the value being of string type. Though, the actual values can be templated (see (Templating). The contents and the variables available in templates depend on the configuration of the particular mechanism, respectively the corresponding override in a rule.
Here is an example:
Values = {
"some-key-1": "value-1",
"some-key-2": "value-2"
}
Templating
Some pipeline mechanisms support templating using Golang Text Templates. Templates can act on all objects described above (Subject, Request, Payload and Values). Which exactly are supported is mechanism specific.
To ease the usage, all sprig functions, except env
and expandenv
, as well as the following functions are available:
urlenc
- Encodes a given string using url encoding. Is handy if you need to generate request body or query parameters e.g. for communication with further systems.atIndex
- Implements python-like access to arrays and takes as a single argument the index to access the element in the array at. With index being a positive values it works exactly the same way, as with the usage of the build-in index function to access array elements. With negative index value, one can access the array elements from the tail of the array. -1 is the index of the last element, -2 the index of the element before the last one, etc.Example:
{{ atIndex 2 [1,2,3,4,5] }}
evaluates to3
(behaves the same way as theindex
function) and{{ atIndex -2 [1,2,3,4,5] }}
evaluates to4
.splitList
- Splits a given string using a separator (part of the sprig library, but not documented). The result is a string array.Example:
{{ splitList "/" "/foo/bar" }}
evaluates to the["", "foo", "bar"]
array.
Imagine, we have a POST
request for the URL http://foobar.baz/zab?foo=bar
, with a header X-Foo
set to bar
value, for which heimdall was able to identify a subject, with ID=foo
and which Attributes
contain an entry email: foo@bar
, then you can generate a JSON object with this information with the following template:
{
"subject_id": {{ quote .Subject.ID }},
"email": {{ quote .Subject.Attributes.email }},
"request_url": {{ quote .Request.URL }},
"foo_value": {{ index .Request.URL.Query.foo 0 | quote }}
"request_method": {{ quote .Request.Method }},
"x_foo_value": {{ .Request.Header "X-Foo" | quote }}
}
Please note how the access to the foo
query parameter is done. Since .Request.URL.Query.foo
returns an array of strings, the first element is taken to render the value for the foo_value
key.
This will result in the following JSON object:
{
"subject_id": "foo",
"email": "foo@bar.baz",
"request_url": "http://foobar.baz/zab?foo=bar",
"foo_value": "bar",
"request_method": "POST",
"x_foo_value": "bar"
}
Imagine, we have a POST
request to the URL http://foobar.baz/zab/1234
, with 1234
being the identifier of a file, which should be updated with the contents sent in the body of the request, and you would like to control access to the aforesaid object using e.g. OpenFGA. This can be achieved with the following authorizer:
id: openfga_authorizer
type: remote
config:
endpoint:
url: https://openfga/stores/files/check
payload: |
{
"user": "user:{{ .Subject.ID }}",
"relation": "write",
"object": "file:{{ splitList "/" .Request.URL.Path | last }}"
}
expressions:
- expression: |
Payload.allowed == true
Please note how the "object"
is set in the payload
property above. When the payload
template is rendered and for the above said request heimdall was able to identify the subject with ID=foo
, following JSON object will be created:
{
"user": "user:foo",
"relation": "write",
"object": "file:1234"
}
You can find further examples as part of mechanism descriptions, supporting templating.
Expressions
Expressions can be used to execute authorization logic. As of today only CEL is supported as expression language. All standard, as well as extension functions are available. Which of the evaluation objects are available to the expression depends on the mechanism.
In addition to the build-in, respectively extension methods and functions, as well as the methods available on the evaluation objects, following functions are available as well:
split
- this function works on strings and expects a separator as a single argument. The result is a string array.Example:
"/foo/bar/baz".split("/")
returns["", "foo", "bar", "baz"]
.regexFind
- this function returns the first (left most) match of a regular expression in the given string.Example:
"abcd1234".regexFind("[a-zA-Z][1-9]")
returns"d1"
.regexFindAll
- this function returns an array of all matches of a regular expression in the given string.Example:
"123456789".regexFindAll("[2,4,6,8]")
returns["2","4","6","8"]
.at
- this function implements python-like access to arrays and takes as a single argument the index to access the element in the array at. With index being a positive values it works exactly the same way, as with the usage of[]
to access array elements. With negative index value, one can access the array elements from the tail of the array. -1 is the index of the last element, -2 the index of the element before the last one, etc.Example:
[1,2,3,4,5].at(2)
returns3
and[1,2,3,4,5].at(-2)
returns4
.last
- this function works on arrays and returns the last element of an array ornil
if the array is empty.Example:
[1,2,3,4,5].last()
returns5
Some examples:
Given the following Payload object
Payload = { "result": true }
a CEL expression to check the result
attribute is set to true
, would look as follows:
Payload.result == true
has(Subject.Attributes.groups) &&
Subject.Attributes.groups.exists(g, g == "admin")
Request.URL.Path.split("/").last()
type(Error) == authentication_error && Error.Source == "foo"
Last updated on Nov 28, 2023