Pipeline Overview

This section explains the available pipeline handler and mechanisms in detail. Before diving onto the details of these, we recommend to make yourself familiar with the concepts.

The general pipeline handlers are:

  • Authenticators inspect HTTP requests, like the presence of a specific cookie, which represents the authentication object of the subject with the service and execute logic required to verify the authentication status and obtain information about that 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 has already been authenticated and the information available about it is valid.

  • Authorizers ensure that the subject obtained via an authenticator step 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.

  • Hydrators enrich the information about the subject obtained in the authenticator step with further information, required by either the endpoint of the upstream service itself or an authorizer step. 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.

  • Mutators finalize the successful execution of the pipeline and transform the available information about the subject into a format expected, respectively required by the upstream service. This ranges from adding a query parameter, to a structured JWT in a specific header.

  • Error Handlers are responsible for execution of logic if any of the handlers described above failed. These range from a simple error response to the client which sent the request to sophisticated handlers supporting complex logic and redirects.

General Configuration

All of the above said handlers must be configured in the pipeline section of Heimdall’s configuration as prototypes for usage in the actual rule definition. With other words only those handlers, which have been configured, can then be reused by a rule.

pipeline:
  authenticators:
    <list of authenticators>
  authorizers:
    <list of authorizers>
  hydrators:
    <list of hydrators>
  mutators:
    <list of mutators>
  error_handlers:
    <list of error handlers>

Each handler configuration entry must contain at least the following properties:

  • id - The unique identifier of a handler. Identifiers are used to reference the required handler from a rule. You can choose whatever identifier, you want. It is just a name. It must however be unique across all defined handlers of a particular general type (like authenticator, authorizer, etc.).

  • type - The specific type of pipeline handler.

Depending on a pipeline handler type, there can be an additional config property, as the name implies, for the definition of handler’s specific configuration. Every handler specific type can be defined as many times as needed in the pipeline definition. 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 e.g. your authenticator definitions could look like this:

pipeline:
  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 pipeline configures two instances of an imaginary authenticator of a specific type bar available via ids foo and zab, as well as two instances of an imaginary authenticator of a specific type bla available 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 a 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 a handler. In more complex cases a rule can reconfigure parts of it (More about rules configuration can be found here). Which parts can be reconfigured are handler specific and described in the documentation of each handler.

Here is an example which configures a couple of prototypes:

pipeline:
  authenticators:
    - id: noop_authn
      type: noop
    - 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: allow_all_authz
      type: allow
    - id: deny_all_authz
      type: deny
    - id: local_authz
      type: local
      config:
        script: |
          if (!heimdall.Subject.Attributes.group_manager.groups["foo"]) {
            raise("user not in the expected group")
          }
  hydrators:
    - id: group_manager
      type: generic
      config:
        endpoint:
          url: http://group-manager.local/groups
          method: GET
        forward_headers:
          - Authorization
        cache_ttl: 1m
  mutators:
    - id: noop_mut
      type: noop
    - id: jwt_mut
      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
      type: redirect
      config:
        to: http://127.0.0.1:4433/self-service/login/browser
        return_to_query_parameter: return_to
        when:
          - error:
              - type: authentication_error
              - type: authorization_error
            request_headers:
              Accept:
                - text/html

Templating

Some pipeline handlers support templating using Golang Text Templates. To ease the usage, all sprig functions as well as a urlenc function are available. Latter is handy if you need to generate request body or query parameters e.g. for communication with further systems. In addition to the above said functions, heimdall makes the following objects and functions available to the template:

  • Subject - object, providing access to all attributes available for the given subject.

    The type of this object is heimdall.Subject and is defined as follows:

    type Subject struct {
    	// The id of the subject
    	ID         string
    	// All attributes known about the subject
    	Attributes map[string]any
    }
  • RequestMethod - function, providing access to the used HTTP method for the given request. Returns a string.

  • RequestURL - function, providing access to the matched URL of the given request. Returns a URL object as defined by Golang net.url.URL. This way access to properties, like Scheme, Host, Path and other URL properties is easily possible. If used as is it is converted to a string.

  • RequestClientIPs - function, providing information about the client IPs known about the request. Returns a string array.

  • RequestHeader - function, expecting the name of a header as input. Returns the value of the header as string if present in the HTTP request. If not present an empty string ("") is returned.

  • RequestCookie - function, expecting the name of a cookie as input. Returns the value of the cookie as string if present in the HTTP request. If not present an empty string ("") is returned.

  • RequestQueryParameter - function, expecting the name of a query parameter as input. Returns the value of the query parameter as string if present in the HTTP request. If not present an empty string ("") is returned.

Example 1. Template, rendering a JSON object

Imagine, we have a POST request for the URL http://foobar.baz/zab, 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 .RequestURL }},
  "request_method": {{ quote .RequestMethod }},
  "x_foo_value": {{ .RequestHeader "X-Foo" | quote }}
}

This will result in the following JSON object:

{
    "subject_id": "foo",
    "email": "foo@bar.baz",
    "request_url": "http://foobar.baz/zab",
    "request_method": "POST",
    "x_foo_value": "bar"
}

You can find further examples as part of handler descriptions, supporting templating.

Scripting

Some authorizers, which verify the presence or values of particular attributes of the subject can make use of ECMAScript 5.1(+). Heimdall uses goja as ECMAScript engine. In addition to the general ECMAScript functionality, heimdall makes following objects and functions to the script:

  • a console object implementing a log function to enable logging from the script. This can become handy during development or debugging. The output is only available if debug log level is set.

  • a heimdall object, which, depending on the authorizer, contains either

    • the Subject object and the request context functions, like RequestMethod (already described in Templating section), or

    • the Payload object, which allows access to the response from remote authorization endpoints.

Example 2. Script, rendering a JSON object

The following script creates the same JSON object, as in the example provided in the previous section, logs however also a statement

console.log("This statement is only present in the logs, if the log level is set to debug")

var data = {
    "subject_id": heimdall.Subject.ID,
    "email": heimdall.Subject.Attributes.email,
    "request_url": heimdall.RequestURL(),
    "request_method": heimdall.RequestMethod(),
    "x_foo_value": heimdall.RequestHeader("X-Foo")
}

data

The result is again the already known JSON object.

{
    "subject_id": "foo",
    "email": "foo@bar.baz",
    "request_url": "http://foobar.baz/zab",
    "request_method": "POST",
    "x_foo_value": "bar"
}
Example 3. Script, checking simple response from an Open Policy Agent endpoint

Imagine you, respectively heimdall, sends something like '{ "input": { "user_id": "foobar", "access": "write" } }' to your OPA service. A typical "yes" or "no" response from OPA will contain a payload like { "result": true } or { "result": false } which obviously contains the authorization status. To check this status, you could use the following script.

heimdall.Payload.result === true

You can find further examples as part of handler descriptions, supporting scripting.

Last updated on Nov 4, 2022