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.

  • Unifiers, which finalize the successful execution of the pipeline and unify the available information about the subject by transforming it 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, 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 rules configuration property. Only those mechanisms, which have been configured, can then be (re-)used by rules.

rules:
  mechanisms:
    authenticators:
      <list of authenticators>
    authorizers:
      <list of authorizers>
    contextualizers:
      <list of contextualizers>
    unifiers:
      <list of unifiers>
    error_handlers:
      <list of error handlers>

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.

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:

rules:
  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 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:

rules:
  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
    unifiers:
    - id: jwt_unifier
      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={{ .Request.URL | urlenc }}
        when:
          - error:
              - type: authentication_error
              - type: authorization_error
            request_headers:
              Accept:
                - text/html

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: string

    The identifier of the subject. This value is set by the authenticator, which was able to authenticate the subject.

  • Attributes: map

    Contains 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:

Example 1. Subject created by an Anonymous Authenticator
Subject = {
  ID: "anonymous",
  Attributes: {}
}
Example 2. Possible Subject created by an OAuth2 Authenticator
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: string

    The HTTP method used, like GET, POST, etc.

  • URL: URL

    The URL of the matched request. This object has the following properties and methods:

    • Scheme: string

      The HTTP scheme part of the url

    • Host: string

      The host part of the url

    • Path: string

      The path part of the url

    • RawQuery: string

      The raw query part of the url.

    • String(): method

      This method returns the URL as valid URL string of a form scheme:host/path?query.

    • Query(): method

      The parsed query with each key-value pair being a string to array of strings mapping.

  • ClientIP: string array

    The 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 the value of it as string. If the header is not present in the HTTP request an empty string ("") is returned.

  • 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.

Here is an example:

Example 3. Example 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:

Example 4. Structured payload

The following JSON object is a typical response from OPA.

Payload = { "result": true }
Example 5. Unstructured payload
Payload = "SomeStringValue"

Templating

Some pipeline mechanisms support templating using Golang Text Templates. To ease the usage, all sprig functions, except env and expandenv, as well as an urlenc function are available. Latter is handy if you need to generate request body or query parameters e.g. for communication with further systems. Templates can act on all objects described above (Subject, Request and Payload). Which exactly are supported is mechanism specific.

Example 6. Template, rendering a JSON object

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"
}

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. Which of the evaluation objects are available to the expression depends on the mechanism.

Some examples:

Example 7. Evaluate Payload object

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

or even simpler:

Payload.result
Example 8. Check whether the user is member of the admin group
has(Subject.Attributes.groups) &&
   Subject.Attributes.groups.exists(g, g == "admin")

Last updated on Dec 26, 2022