Heimdall Authorization

Heimdall is a simple rule-based authorization engine whose main responsibility is to accept an authorization request in form of an HTTP payload and return a decision whether the request is allowed or denied in form of an HTTP response code. You can put this authorization engine behind API gateways, and reverse proxies to protect your APIs and services and allow them to formulate an authorization request to CAS, receive a response and translate that back to the caller.

In Norse mythology, Heimdall is a god and gatekeeper who keeps watch for invaders and is attested as possessing foreknowledge and keen senses. As gatekeeper, he is responsible for the rainbow bridge Bifrost and keeps a watchful eye on passengers.

The general flow can be summarized using the following steps:

  • Authorizable resources are registered with CAS
    • …with the appropriate method, URI, namespace and context
    • …with the appropriate authorization policies
  • Authorization request is submitted to CAS
    • …with the appropriate principal/subject
    • …with the appropriate method, URI, namespace and context
  • CAS locates the matching authorizable resource based on the request
  • …and then determines the principal/subject based on the request
  • CAS then consults the authorization engine to make a decision based on the resource, the principal and the request
  • CAS returns a response to the caller, either accepting or denying the request
:information_source: Usage

Note that CAS is simply acting as the policy definition point (PDP) as well as the policy information point (PIP). The authorization enforcement (PEP) must happen somewhere else by the calling party, which typically happens to be an API gateway or nginx reverse proxy, etc.

Configuration

Heimdall authorization support is enabled by including the following dependency in the overlay:

1
2
3
4
5
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-heimdall</artifactId>
    <version>${cas.version}</version>
</dependency>
1
implementation "org.apereo.cas:cas-server-support-heimdall:${project.'cas.version'}"
1
2
3
4
5
6
7
8
9
dependencyManagement {
    imports {
        mavenBom "org.apereo.cas:cas-server-support-bom:${project.'cas.version'}"
    }
}

dependencies {
    implementation "org.apereo.cas:cas-server-support-heimdall"
}
1
2
3
4
5
6
7
8
9
10
dependencies {
    /*
        The following platform references should be included automatically and are listed here for reference only.

        implementation enforcedPlatform("org.apereo.cas:cas-server-support-bom:${project.'cas.version'}")
        implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        
    */
    implementation "org.apereo.cas:cas-server-support-heimdall"
}

The following settings and properties are available from the CAS configuration catalog:

The configuration settings listed below are tagged as Required in the CAS configuration metadata. This flag indicates that the presence of the setting may be needed to activate or affect the behavior of the CAS feature and generally should be reviewed, possibly owned and adjusted. If the setting is assigned a default value, you do not need to strictly put the setting in your copy of the configuration, but should review it nonetheless to make sure it matches your deployment expectations.

  • cas.heimdall.json.location=
  • The location of the resource. Resources can be URLs, or files found either on the classpath or outside somewhere in the file system.

    In the event the configured resource is a Groovy script, specially if the script set to reload on changes, you may need to adjust the total number of inotify instances. On Linux, you may need to add the following line to /etc/sysctl.conf: fs.inotify.max_user_instances = 256.

    You can check the current value via cat /proc/sys/fs/inotify/max_user_instances.

    In situations and scenarios where CAS is able to automatically watch the underlying resource for changes and detect updates and modifications dynamically, you may be able to specify the following setting as either an environment variable or system property with a value of false to disable the resource watcher: org.apereo.cas.util.io.PathWatcherService.

    org.apereo.cas.configuration.model.SpringResourceProperties.

    How can I configure this property?

    The configuration settings listed below are tagged as Optional in the CAS configuration metadata. This flag indicates that the presence of the setting is not immediately necessary in the end-user CAS configuration, because a default value is assigned or the activation of the feature is not conditionally controlled by the setting value. In other words, you should only include this field in your configuration if you need to modify the default value or if you need to turn on the feature controlled by the setting.

    Configuration Metadata

    The collection of configuration properties listed in this section are automatically generated from the CAS source and components that contain the actual field definitions, types, descriptions, modules, etc. This metadata may not always be 100% accurate, or could be lacking details and sufficient explanations.

    Be Selective

    This section is meant as a guide only. Do NOT copy/paste the entire collection of settings into your CAS configuration; rather pick only the properties that you need. Do NOT enable settings unless you are certain of their purpose and do NOT copy settings into your configuration only to keep them as reference. All these ideas lead to upgrade headaches, maintenance nightmares and premature aging.

    YAGNI

    Note that for nearly ALL use cases, declaring and configuring properties listed here is sufficient. You should NOT have to explicitly massage a CAS XML/Java/etc configuration file to design an authentication handler, create attribute release policies, etc. CAS at runtime will auto-configure all required changes for you. If you are unsure about the meaning of a given CAS setting, do NOT turn it on without hesitation. Review the codebase or better yet, ask questions to clarify the intended behavior.

    Naming Convention

    Property names can be specified in very relaxed terms. For instance cas.someProperty, cas.some-property, cas.some_property are all valid names. While all forms are accepted by CAS, there are certain components (in CAS and other frameworks used) whose activation at runtime is conditional on a property value, where this property is required to have been specified in CAS configuration using kebab case. This is both true for properties that are owned by CAS as well as those that might be presented to the system via an external library or framework such as Spring Boot, etc.

    :information_source: Note

    When possible, properties should be stored in lower-case kebab format, such as cas.property-name=value. The only possible exception to this rule is when naming actuator endpoints; The name of the actuator endpoints (i.e. ssoSessions) MUST remain in camelCase mode.

    Settings and properties that are controlled by the CAS platform directly always begin with the prefix cas. All other settings are controlled and provided to CAS via other underlying frameworks and may have their own schemas and syntax. BE CAREFUL with the distinction. Unrecognized properties are rejected by CAS and/or frameworks upon which CAS depends. This means if you somehow misspell a property definition or fail to adhere to the dot-notation syntax and such, your setting is entirely refused by CAS and likely the feature it controls will never be activated in the way you intend.

    Validation

    Configuration properties are automatically validated on CAS startup to report issues with configuration binding, specially if defined CAS settings cannot be recognized or validated by the configuration schema. Additional validation processes are also handled via Configuration Metadata and property migrations applied automatically on startup by Spring Boot and family.

    Indexed Settings

    CAS settings able to accept multiple values are typically documented with an index, such as cas.some.setting[0]=value. The index [0] is meant to be incremented by the adopter to allow for distinct multiple configuration blocks.

    Authorization Request

    The authorization request is a simple payload that is sent to the Heimdall authorization engine using the endpoint /heimdall/authorize via a POST. The payload has the following structure:

    1
    2
    3
    4
    5
    6
    7
    8
    
    {
      "method" : "POST",
      "uri" : "/api/example?hello=world",
      "namespace" : "API_EXAMPLE",
      "context" : {
        "key" : "value"
      }
    }
    

    …which is trying to ask CAS:

    Is the request to /api/example?hello=world, owned by API_EXAMPLE, using the HTTP method POST, allowed?

    The following elements are supported:

    Field Description
    method The requested HTTP method to allow or deny.
    uri The request URI intended for access and invocation by the caller.
    namespace Logical name for the owner of the API or resource in question.
    context Free-form key-value pairs for more advanced decisions based on arbitrary contextual data.

    Typical responses include 200, 401 or 403.

    Authorization Principal

    The authorization request is expected to provide an Authorization header using the Bearer or Basic schemes (Authorization: Bearer/Basic ...). The token in the header must indicate the who, the subject or the authorization principal that wants to access the resource using the details specified in the request.

    The authorization header value can be one of the following:

    • An OpenID Connect ID token, passed as a Bearer token, produced by CAS when acting as a OpenID Connect Provider.
    • A JWT access token, passed as a Bearer token, produced by CAS when acting as an OAuth or OpenID Connect identity provider.
    • An opaque access token (i.e. AT-1-...), passed as a Bearer token, produced by CAS when acting an OAuth or OpenID Connect identity provider.
    • A valid Base64-encoded username:password, passed as a Basic token, that can be accepted by the CAS authentication engine.

    Claims or attributes from all token types are extracted and attached to the final principal, which is then passed to the authorization policy engine to make decisions.

    Authorization Resources

    Authorizable resources and APIs that are to be supported and protected by Heimdall are expected to be registered with CAS. This is done by defining and configuring a list of resources and their associated owners via flat JSON files. For easier discovery, files are named and thus categorized by API owner or group (i.e. API_EXAMPLE.json) that describe a collection of APIs in that namespace:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    {
      "@class": "org.apereo.cas.heimdall.authorizer.resource.AuthorizableResources",
      "resources": [
        "java.util.ArrayList",
        [
          {
            "@class": "org.apereo.cas.heimdall.authorizer.resource.AuthorizableResource",
            "id": 1,
            "pattern": "/api/example.*",
            "method": "PUT",
            "enforceAllPolicies": false,
            "policies": [ "java.util.ArrayList", [
                {}
            ]],
            "properties" : {
                "@class" : "java.util.HashMap",
                "key" : "value"
            }
          }
        ]
      ],
      "namespace": "API_EXAMPLE"
    }
    

    Note that policies are loaded, sorted and evaluated using the order in which they are defined in the file. If you have policies that operate on patterns, you may want to ensure that the most specific policies are listed first.

    :information_source: Usage

    Remember that the file name is mostly irrelevant. While we recommend reasonable naming conventions, the namespace field inside the policy is really the piece that determines its owner.

    The authorization policies owned by the indicated namespace and resource support the following elements:

    Field Description
    id Unique numeric identifier for this resource.
    pattern The URI regular expression pattern that describes the resource or API endpoint.
    method The HTTP method (as a regular expression pattern, or * for all) that is allowed to access the resource.
    policies A list of policies that are attached to the resource to allow or deny access.
    enforceAllPolicies Whether all policies must be consulted to authorize the request. Default is false.
    properties Arbitrary key-value pairs attached to the resource for advanced decision making.

    Custom

    You can also build your own repository implementation to register and load authorizable resources. This may be done by providing a dedicated implementation of AuthorizableResourceRepository and registering it with the runtime:

    1
    2
    3
    4
    
    @Bean
    public AuthorizableResourceRepository authorizableResourceRepository(
        return new MyResourceRepository();
    }
    

    See this guide to learn more about how to register configurations into the CAS runtime.

    Authorization Policies

    Policies are the rules attached to resources to allow or deny access. Each authorizable resource may have one or more policies assigned to it. Policies are evaluated in the order in which they are defined for the resource. The following policies are supported by CAS:

    • An authorization policy that can accept an inline or external Groovy script to make decisions:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.GroovyAuthorizationPolicy",
        "script" :
          '''
            groovy {
                def iAllowThis = true
                return iAllowThis
                  ? AuthorizationResult.granted("OK")
                  : AuthorizationResult.denied("NOPE")
            }
          '''
      }
      

      The following parameters are passed to the script:

      Parameter Description
      resource The matched AuthorizableResource object.
      request The supplied AuthorizationRequest object.
      applicationContext Reference to the Spring ApplicationContext reference.
      logger The object responsible for issuing log messages such as logger.info(...).
    • An authorization policy that fetches group memberships for the principal from Grouper and makes decisions based on required groups:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredGrouperGroupsAuthorizationPolicy",
        "groups" : [ "java.util.HashSet", [ "a:b:c" ] ]
      }
      
    • An authorization policy that fetches permissions for the principal from Grouper using attribute definitions or roles and allows or denied access based on whether permissions are found:

      1
      2
      3
      4
      5
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredGrouperPermissionsAuthorizationPolicy",
        "attributeDefinition" : "a:b:c",
        "roleName": "..."
      }
      
    • An authorization policy that checks for the presence of required attributes in the authorization principal’s profile:

      1
      2
      3
      4
      5
      6
      7
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredAttributesAuthorizationPolicy",
        "attributes" : {
          "@class" : "java.util.HashMap",
          "memberOf" : [ "java.util.HashSet", [ ".*admin.*" ] ]
        }
      }
      
    • An authorization policy that checks for the absence of indicated attributes in the authorization principal’s profile:

      1
      2
      3
      4
      5
      6
      7
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RejectedAttributesAuthorizationPolicy",
        "attributes" : {
          "@class" : "java.util.HashMap",
          "memberOf" : [ "java.util.HashSet", [ ".*admin.*" ] ]
        }
      }
      
    • An authorization policy that requires a specific acr claim in the principal’s profile:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredACRAuthorizationPolicy",
        "acrs" : [ "java.util.HashSet", [ ".*" ] ]
      }
      
    • An authorization policy that requires a specific amr claim in the principal’s profile:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredAMRAuthorizationPolicy",
        "amrs" : [ "java.util.HashSet", [ ".*" ] ]
      }
      
    • An authorization policy that requires a specific aud claim in the principal’s profile:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredAudienceAuthorizationPolicy",
        "audience" : [ "java.util.HashSet", [ ".*" ] ]
      }
      
    • An authorization policy that requires a specific iss claim in the principal’s profile:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredIssuerAuthorizationPolicy",
        "issuer" : "^http://.*"
      }
      
    • An authorization policy that requires the indicated scopes in the principal’s profile:

      1
      2
      3
      4
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RequiredScopesAuthorizationPolicy",
        "scopes" : [ "java.util.HashSet", [ "profile" ] ]
      }
      
    • An authorization policy can be outsources to a REST API that can make decisions based on the request and the resource:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      {
          "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.RestfulAuthorizationPolicy",
          "url": "https://api.example.org",
          "method": "POST",
          "headers": {
            "@class": "java.util.LinkedHashMap",
            "header": "value"
          }
      }
      
      • The request body will contain a map to present the request and the resource JSON payloads.
      • Authorized requests are expected to receive a 200 response code.
      • The url and header values can be constructed using the Spring Expression Language
    • An authorization policy that passes the request to OpenFGA to make decisions:

      1
      2
      3
      4
      5
      6
      7
      
      {
        "@class": "org.apereo.cas.heimdall.authorizer.resource.policy.OpenFGAAuthorizationPolicy",
        "token": "...",
        "apiUrl": "...",
        "storeId": "...",
        "relation": "..."  
      }
      

      The following parameters are passed to OpenFGA:

      Parameter Description
      token [1] The bearer authorization token passed via the Authorization header.
      apiUrl [1] OpenFGA base API endpoint that ultimately invokes the check API.
      storeId [1] The authorization store identifier.
      relation [1] The relation or the type of access in the authorization tuple; defaults to owner.

      The object field in the API request is composed of the following elements:

      1
      
      $REQUEST_NAMESPACE + ':' + $REQUEST_METHOD + ':' + $REQUEST_URI
      

      [1] This field supports the Spring Expression Language syntax.

    Actuator Endpoints

    The following endpoints are provided by CAS:

     Fetch all authorizable resources for namespace.

     Fetch all authorizable resources.

     Fetch all authorizable resources for namespace by id.

     Fetch authorizable resource matching the given authorization request in the body.