wip: Multiple principals for a Subject
Related issue(s)
closes #921
Checklist
- [x] I agree to follow this project's Code of Conduct.
- [x] I have read, and I am following this repository's Contributing Guidelines.
- [x] I have read the Security Policy.
- [x] 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
Subjectobject to support multiplePrincipals(the different entities mentioned above). That way theSubjectbecomes an object holding the different authenticated principals, each having at least anIDandDataattributes. Starting with #1487 theSubjectobject has been made immutable. This behavior applies to thePrinipalobjects as well.This way, a
Principalis very similar to the oldSubjectand 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
Subjectis 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 thePrincipalentries representing theSubject. Even it does not have anIDproperty any more, it has a newID()andAttributes()functions, which returns the theID, respectively theAttributesof thedefaultprincipal - 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
subjectproperty of all authenticators have been renamed toprincipal(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_errorproperty set totrue) or was not responsible for the provided authentication data in the request. However, sinceallow_fallback_on_errorpertains to pipeline behavior rather than authenticator behavior, this property has been removed from individual authenticators (BREAKING CHANGE).Instead, a new optional
groupproperty 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 thegroupproperty is not specified, the authenticator step defaults to the "default" group. Here some examples:- Verify authentication information issued Google or fallback to anonymous
Noexecute: - authenticator: keycloak - authenticator: anon - # further pipeline stepsgroupis used here. This pipeline basically implements functionality, which is already possible today - Verify authentication information issued for user and a device
The first two authenticators belong to the default group and the third one to the device group. The resultingexecute: - authenticator: keycloak - authenticator: anon - authenticator: keycloak_device group: device - # further pipeline stepsSubjectwill contain adefaultprincipal and adeviceprincipal 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.
- Verify authentication information issued Google or fallback to anonymous
-
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-JWTheader 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, theAuthorizationheader represents the user and describes its permissions through the scope claim- authenticator: jwt_env_authenticator group: env - authenticator: jwt_user_authenticatorwith the
jwt_env_authenticatorbeing configured to extract the token from theAuthorizationheader and thejwt_user_authenticatorbeing configured to extract the token from theX-Env-JWTheader.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 thejwt_sourcein 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.
Codecov Report
Attention: Patch coverage is 97.72727% with 3 lines in your changes are missing coverage. Please review.
Project coverage is 89.30%. Comparing base (
32c53c8) to head (7522727).
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
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.