Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: Multiple principals for a Subject #1317

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

Conversation

dadrus
Copy link
Owner

@dadrus dadrus commented Apr 4, 2024

Related issue(s)

closes #921

Checklist

  • I agree to follow this project's Code of Conduct.
  • I have read, and I am following this repository's Contributing Guidelines.
  • I have read the Security Policy.
  • I have referenced an issue describing the bug/feature request.
  • I have added tests that prove the correctness of my implementation.
  • I have updated the documentation.

Background

In heimdall the term Subject is defined to represent the source of a request which is created upon successful authentication. This way, a Subject may be any entity, such as a person, a service, or something else. Until now, a Subject was represented by the following JSON schema

{
    "type": "object",
    "additionalProperties": false,
    "required": [ "ID" ],
    "properties": {
        "ID": {
            "description": "The unique identifier of the subject",
            "type": "string"
        },
       "Attributes": {
            "description": "Optional attributes describing the data used during the authentication of the subject",
            "type": "object",
            "uniqueItems": true
       }
    }
}

with ID being a unique identifier of the subject and Attributes representing a dictionary of attributes related to the authenticated subject. These attributes could be for examples claims from a JWT used to authenticate the subject.

This abstraction was enough for long time. But it has its drawbacks. In a real life a user wanting accessing an API, may use for example a laptop equipped with a client certificate from which the actual request is sent to the aforesaid API. It can be an IoT device, like e.g. a heating system, an end customer is using. It may even be an environment to which a user should authenticate first. In all these cases, we're actually talking about different and complementing authentication aspects related to the same request, but representing different entities (like a user and a device).

Reasoning, why not going for new authorizer types instead

It would simply lead to code bloating and potentially to a lot of duplication as for each authenticator, there would be a need for an authorizer doing the same thing.

Description

NOTE: This PR is in a very early stage. The text below describes the current ideas which might change over time (see also the history of this PR description) or during the implementation.

For the above said reasons, this PR introduces the following changes:

  • It refactors the Subject object to support multiple Principals (the different entities mentioned above). That way the Subject becomes an object holding the different authenticated principals, each having at least an ID and Data attributes. Starting with refactor!: Subject has been made immutable #1487 the Subject object has been made immutable. This behavior applies to the Prinipal objects as well.

    This way, a Principal is very similar to the old Subject and can be represented by the following JSON schema:

    {
        "type": "object",
        "additionalProperties": false,
        "required": [ "ID" ],
        "properties": {
            "ID": {
                "description": "The unique identifier of the subject",
                "type": "string"
            },
            "Attributes": {
                "description": "Optional data used during the authentication of the subject",
                "type": "object",
                "uniqueItems": true
            }
        }
    }

    The new Subject is then just an object in sense of a JSON object.

    {
        "<principal name 1>": {
            "ID": "some identifier",
            "Attributes": {}
        },
        "<principal name 2>": {
            "ID": "some identifier",
           "Attributes": {}
        },
        ...
    }

    With <principal name ...> being the Principal entries representing the Subject. Even it does not have an ID property any more, it has a new ID() and Attributes() functions, which returns the the ID, respectively the Attributes of the default principal - the principal, which has been created by an authentication stage marked accordingly (see below).

  • Since authenticators do now create principals and populate a Subject with them, the subject property of all authenticators have been renamed to principal (BREAKING CHANGE).

  • The semantics of the authentication stage have been updated. Previously, when multiple authenticators were specified, subsequent ones would only execute if the preceding authenticator either failed and allowed fallback (via the allow_fallback_on_error property set to true) or was not responsible for the provided authentication data in the request. However, since allow_fallback_on_error pertains to pipeline behavior rather than authenticator behavior, this property has been removed from individual authenticators (BREAKING CHANGE).

    Instead, a new optional group property has been introduced at the authenticator step level. This property enables grouping multiple authenticators, with the semantics within a group being OR-based and between the groups AND-based. This means that exactly one authenticator in a group must succeed. Subsequent authenticators in the group are only executed if the preceding one was either not responsible or failed. The group's name determines the principal created by that group. Definition of multiple groups is possible as well, and if the group property is not specified, the authenticator step defaults to the "default" group. Here some examples:

    • Verify authentication information issued Google or fallback to anonymous
      execute:
      - authenticator: keycloak
      - authenticator: anon
      - # further pipeline steps
      No group is used here. This pipeline basically implements functionality, which is already possible today
    • Verify authentication information issued for user and a device
      execute:
      - authenticator: keycloak
      - authenticator: anon
      - authenticator: keycloak_device
        group: device
      - # further pipeline steps
      The first two authenticators belong to the default group and the third one to the device group. The resulting Subject will contain a default principal and a device principal similar to what is shown below
      {
          "default": {
              "ID": "some identifier",
              "Attributes": { ... }
          },
          "device": {
              "ID": "some identifier",
             "Attributes": { ... }
          },
      }

    Obviously, if groups are not defined, the behavior is exactly as it was before this PR.

  • All of that slightly affects how subject related data can be accessed. Here are two examples highlighting the differences.

    Old

    some_property: |
      { 
          # accessing the ID of the Subject
          "user": {{ .Subject.ID | quote }},
    
          # accessing iss claim from the JWT used to authenticate the subject
          "jwt_claim": {{ .Subject.Attributes.iss | quote }}
      }

    New

    some_property: |
       { 
           # accessing the ID of the Principal created by the default authenticator stage 
           "user": {{ .Subject.ID | quote }}, # or alternatively {{ .Subject.default.ID | quote }},
    
           # the following was not possible before
           # accessing the ID of the Principal created by the authenticator stage named "device"
           "device": {{ .Subject.device.ID | quote }}
       }

Examples

With that in place, one can now chain and combine multiple authentication mechanisms. Here examples for the implementation of the requirements described in #921.

  • Access to a staging environment. Only project members should be able to access the services (via e.g. a browser) to see and test the new deployed features. There is also an IAM in the staging environment itself which manages the "customers". So, the first IAM manages the access to the environment . The X-Env-JWT header certifies that the request has been routed through an authorized gateway (so access to the environment was legitimate). And the second IAM represents the actual users of the services deployed. Here, the Authorization header represents the user and describes its permissions through the scope claim

    - authenticator: jwt_env_authenticator
      group: env
    - authenticator: jwt_user_authenticator

    with the jwt_env_authenticator being configured to extract the token from the Authorization header and the jwt_user_authenticator being configured to extract the token from the X-Env-JWT header.

    This example indicates that the two mechanisms referenced in the above steps are pretty much the same. The only difference would be the configuration of the jwt_source, which extracts the token from different headers. this duplication is a tradeoff between simplicity in the rules and duplication in the config. Reconfiguration of the jwt_source in a pipeline step was however never possible before. Opening it to the pipeline steps is possible, would however introduce a source for errors.

  • Verification that the request came over a specific intermediary. Depending on the path the request took, the gateway issues an additional token, e.g. X-Caller-ID, which is then present in addition to the token in the Authorization header.

    - authenticator: jwt_authenticator
      group: request_source
    - authenticator: oauth2_auth

Current PR Status

In a very early stage. The changes implemented so far is the update of the Subject to let it be a map of Principal objects and have the code compilable.

Copy link

codecov bot commented Apr 5, 2024

Codecov Report

Attention: Patch coverage is 97.72727% with 3 lines in your changes missing coverage. Please review.

Project coverage is 89.30%. Comparing base (32c53c8) to head (7522727).
Report is 52 commits behind head on main.

Files with missing lines Patch % Lines
internal/accesscontext/access_context.go 75.00% 1 Missing ⚠️
...l/handler/middleware/grpc/accesslog/interceptor.go 85.71% 1 Missing ⚠️
internal/rules/default_execution_condition.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1317      +/-   ##
==========================================
+ Coverage   89.25%   89.30%   +0.04%     
==========================================
  Files         270      271       +1     
  Lines        8870     8880      +10     
==========================================
+ Hits         7917     7930      +13     
+ Misses        704      703       -1     
+ Partials      249      247       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@dadrus dadrus marked this pull request as draft July 1, 2024 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Authorizer for access token verification
1 participant