Principal = {
  ID: "anonymous",
  Attributes: {}
}Objects, Templating & Co
The dynamic nature of mechanisms is given by the ability to work on different objects and by using templates and expressions to implement various use cases.
E.g. in one case, you want to have access to a particular request header. In another case you would like to add specific data to the resulting JWT created by heimdall. And in yet another case, you may want to check whether some expectations apply. These capabilities are described on this page.
Objects
Objects represent state during the execution of a particular rule. These are entities either created or used by specific mechanisms, and can represent things like the actual request, the authenticated subject of the request, and many others.
Principal
A Principal represents an identity of an entity associated with the current request — such as a user, service, or machine — and verified by a particular authenticator. Principals are immutable and ephemeral — they exist only for the lifetime of a single request.
Each principal has the following properties:
ID: stringThe unique identifier of the principal.
Attributes: mapA key-value map containing all known attributes about the principal.
Each object of this type can be represented as a JSON object. Here are some examples:
Principal = {
  ID: "foobar",
  Attributes: {
    "sub": "foobar",
    "exp": "1670600805",
    "jti": "7b91ed8a-0251-4e02-8d51-9792785851e8",
    "iat": "1670600305",
    "iss": "https://testauthserver.local",
    "nbf": "1670600305",
    "extra": {
        "foo": ["bar", "baz"]
    }
  }
}Subject
A Subject is a higher-order concept — a key-value collection of Principals verified by all successfully executed authenticators, where the keys represent principal names and the values are the corresponding Principal objects.
Like Principals, Subjects are immutable and ephemeral, existing only for the lifetime of a single request.
In addition to its Principal collection, each Subject exposes the following virtual properties:
ID: stringThe unique identifier of the Subject. Its value is taken from the Principal named "default".
Attributes: mapA key-value map containing all attributes of the Principal named "default".
Each object of this type can be thought as a JSON object. Here an example:
Subject = {
  "default": Principal {
    ID: "62302d83-da01-4ba6-9440-119728757455",
    Attributes: {
      "email": "john.doe@example.com"
    }
  },
  "device": Principal {
    ID: "597ef6b5-64fc-49e3-ad1e-7599046908d9",
    Attributes: {
      "os": "Ubuntu 25.04"
    }
  }
}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:
Captures: mapAllows accessing of the values captured by the named wildcards used in the matching path expression of the rule.
Host: stringThe host part of the url.
Hostname(): methodThis method returns the host name stripping any valid port number if present.
Port(): methodReturns the port part of the
Host, without the leading colon. IfHostdoesn’t contain a valid numeric port, returns an empty string.Path: stringThe path part of the url.
Query(): methodThe parsed query with each key-value pair being a string to array of strings mapping.
RawQuery: stringThe raw query part of the url.
Scheme: stringThe HTTP scheme part of the url.
String(): methodThis method returns the URL as valid URL string of a form
scheme:host/path?query.
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-Fromheader (see also 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 returnedstringis 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 Acceptheader, 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-Typeheader. Supported content types are any MIME types withjsonoryamlsubtype, 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 4. Example resultsIf the
Content-Typeheader is set toapplication/jsonand 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-Typeheader is set toapplication/yamland the actual request body is a valid YAML object, shown belowcontext: heimdallThe call to the
Body()function will return{ "context": "heimdall" }representation as a map.If the
Content-Typeheader is set toapplication/x-www-form-urlencodedand the actual request body is a valid object, shown belowcontext=heimdallThe 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/abc",
    RawQuery: "baz=zab&baz=bar&foo=bar",
    Captures: { "value": "abc" }
  },
  ClientIP: ["127.0.0.1", "10.10.10.10"]
}Outputs
This object represents a pipeline execution specific key value map. It is used by pipeline steps to store or read results of particular step executions. Step id is used as a key and the value is the corresponding result.
Example:
Outputs = {
    "id_1": ["a", "b"],
    "id_2": { "foo": "bar", "baz": false }
}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 a rule and is available in if CEL expressions of Error Handlers. Following properties are available:
Source: stringThe ID of the mechanism, which raised the error.
StepID: stringThe ID of the pipeline step, 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 mechanisms support templating using Golang Text Templates. Templates can act on all objects described above (Principal, Subject, Outputs, 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 built-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 theindexfunction) 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 https://foobar.baz/zab?foo=bar, with a header X-Foo set to bar value, for which heimdall was able to identify a subject consisting of a single "default" principal 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 }},
  "whatever": {{ .Outputs.whatever | quote }}
}Please note that .Subject.ID and .Subject.Attribute is a syntactic sugar for .Subject.default.ID, respectively .Subject.default.Attributes. | 
Please also 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": "https://foobar.baz/zab?foo=bar",
    "foo_value": "bar",
    "request_method": "POST",
    "x_foo_value": "bar",
    "whatever": "some value"
}Imagine, we have a POST request to the URL https://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:{{ .Request.URL.Captures.id }}"
    }
  expressions:
  - expression: |
      Payload.allowed == truePlease 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 conditional logic. Currently, only CEL is supported as expression language. All standard CEL functions, as well as extension functions, are available. The set of available evaluation objects depends on the specific mechanism in use.
In addition to the built-in CEL functions, extension methods, and methods on evaluation objects, the following custom functions are also available:
at- Provides Python-like access to array elements. A positive index behaves like standard[]array access. A negative index accesses elements from the end of the array (-1is the last element,-2the second-last, etc.).Example:
[1,2,3,4,5].at(2)returns3and[1,2,3,4,5].at(-2)returns4.last- returns the last element of an array, or nil if the array is empty.Example:
[1,2,3,4,5].last()returns5regexFind- returns the first (leftmost) match of a regular expression within a string.Example:
"abcd1234".regexFind("[a-zA-Z][1-9]")returns"d1".regexFindAll- returns an array of all matches of a regular expression within a string.Example:
"123456789".regexFindAll("[2,4,6,8]")returns["2","4","6","8"].split- splits a string by the given separator and returns an array of strings.Example:
"/foo/bar/baz".split("/")returns["", "foo", "bar", "baz"].networks- accepts a single CIDR string or an array of CIDR strings, and returns a matcher object that can be used to check whether an IP address belongs to one of the specified ranges.Example:
Request.ClientIPAddresses.all(ip in networks(["172.16.0.0/12", "192.168.0.0/16"]))checks whether all IPs in theRequest.ClientIPAddressesarray belong to the specified IP ranges.
Some examples:
Given the following Payload object
Payload = { "result": true }a CEL expression to check whether the result attribute is set to true, would look as follows:
Payload.result == truehas(Subject.Attributes.groups) &&
   Subject.Attributes.groups.exists(g, g == "admin")As with templates Subject.ID and Subject.Attributes is a syntactic sugar for Subject.default.ID, respectively Subject.default.Attributes
Request.URL.Path.split("/").last()type(Error) == authentication_error && Error.Source == "foo"Last updated on Oct 22, 2025