Skip to content

Commit

Permalink
feat: Configurable fallback of authenticators even if the verificatio…
Browse files Browse the repository at this point in the history
…n of the credentials fails (#134)

fix: Basic Auth authenticator added to the schema and can now be configured (#133)
  • Loading branch information
dadrus authored Aug 1, 2022
1 parent 99a2893 commit 1336777
Show file tree
Hide file tree
Showing 26 changed files with 588 additions and 87 deletions.
16 changes: 16 additions & 0 deletions docs/content/docs/configuration/pipeline/authenticators.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ The identifier of the subject to be verified.
+
The password of the subject to be verified.

* *`allow_fallback_on_error`*: _boolean_ (optional, overridable)
+
If set to `true`, allows the pipeline to fall back to the next authenticator in the pipeline if this one fails to verify the credentials. Defaults to `false`.

.Configuration of Basic Auth authenticator
====
[source, yaml]
Expand Down Expand Up @@ -120,6 +124,10 @@ Where to extract the subject id from the identity info endpoint response, as wel
+
How long to cache the response. If not set, response caching if disabled. The cache key is calculated from the `identity_info_endpoint` configuration and the actual authentication data value.

* *`allow_fallback_on_error`*: _boolean_ (optional, overridable)
+
If set to `true`, allows the pipeline to fall back to the next authenticator in the pipeline if this one fails to verify the credentials. Defaults to `false`.

.Configuration of Generic authenticator to work with session cookies
====
Expand Down Expand Up @@ -192,6 +200,10 @@ Where to extract the subject id from the introspection endpoint response, as wel
+
How long to cache the response. If not set, caching of the introspection response is based on the available token expiration information. To disable caching, set it to `0s`. If you set the ttl to a custom value > 0, the expiration time (if available) of the token will be considered. The cache key is calculated from the `introspection_endpoint` configuration and the value of the access token.

* *`allow_fallback_on_error`*: _boolean_ (optional, overridable)
+
If set to `true`, allows the pipeline to fall back to the next authenticator in the pipeline if this one fails to verify the credentials. Defaults to `false`.

.Minimal possible configuration
====
[source, yaml]
Expand Down Expand Up @@ -235,6 +247,10 @@ Where to extract the subject id from the JWT, as well as which attributes to use
+
How long to cache the key from the JWKS response, which was used for signature verification purposes. If not set, Heimdall will cache this key for 10 minutes and not call JWKS endpoint again if the same `kid` is referenced in an JWT and same JWKS endpoint is used. The cache key is calculated from the `jwks_endpoint` configuration and the `kid` referenced in the JWT.

* *`allow_fallback_on_error`*: _boolean_ (optional, overridable)
+
If set to `true`, allows the pipeline to fall back to the next authenticator in the pipeline if this one fails to verify the credentials. Defaults to `false`.

.Minimal possible configuration
====
[source, yaml]
Expand Down
39 changes: 24 additions & 15 deletions docs/content/docs/configuration/reference/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,19 @@ metrics:
pipeline:
authenticators:
- id: "noop_authenticator"
- id: noop_authenticator
type: noop
- id: "anonymous_authenticator"
- id: anonymous_authenticator
type: anonymous
- id: "unauthorized_authenticator"
- id: unauthorized_authenticator
type: unauthorized
- id: "kratos_session_authenticator"
- id: foo
type: basic_auth
config:
user_id: bar
password: baz
allow_fallback_on_error: true
- id: kratos_session_authenticator
type: generic
config:
identity_info_endpoint:
Expand All @@ -125,7 +131,8 @@ pipeline:
session:
subject_attributes_from: "@this"
subject_id_from: "identity.id"
- id: "hydra_authenticator"
allow_fallback_on_error: true
- id: hydra_authenticator
type: oauth2_introspection
config:
introspection_endpoint:
Expand All @@ -149,7 +156,8 @@ pipeline:
session:
subject_attributes_from: "@this"
subject_id_from: "sub"
- id: "jwt_authenticator"
allow_fallback_on_error: true
- id: jwt_authenticator
type: jwt
config:
jwks_endpoint:
Expand All @@ -171,13 +179,14 @@ pipeline:
subject_attributes_from: "@this"
subject_id_from: "identity.id"
cache_ttl: 5m
allow_fallback_on_error: true
authorizers:
- id: "allow_all_authorizer"
- id: allow_all_authorizer
type: allow
- id: "deny_all_authorizer"
- id: deny_all_authorizer
type: deny
- id: "keto_authorizer"
- id: keto_authorizer
type: remote
config:
endpoint:
Expand All @@ -189,13 +198,13 @@ pipeline:
script: "heimdall.Payload.response === true"
forward_response_headers_to_upstream:
- bla-bar
- id: "attributes_based_authorizer"
- id: attributes_based_authorizer
type: local
config:
script: "console.log('New JS script')"
hydrators:
- id: "subscription_hydrator"
- id: subscription_hydrator
type: generic
config:
endpoint:
Expand All @@ -204,7 +213,7 @@ pipeline:
headers:
bla: bla
payload: http://foo
- id: "profile_data_hydrator"
- id: profile_data_hydrator
type: generic
config:
endpoint:
Expand All @@ -213,17 +222,17 @@ pipeline:
foo: bar
mutators:
- id: "jwt"
- id: jwt
type: jwt
config:
ttl: 5m
claims: "{'user': {{ quote .Subject.ID }} }"
- id: "bla"
- id: bla
type: header
config:
headers:
foo-bar: bla
- id: "blabla"
- id: blabla
type: cookie
config:
cookies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ As described in the link:{{< relref "/docs/getting_started/concepts.adoc" >}}[Co

* List of link:{{< relref "/docs/configuration/pipeline/authenticators.adoc" >}}[authenticators] using `authenticator` as key, followed by the required authenticator `id`. Authenticators following the first defined in the list are used by Heimdall as fallback. That is, if first authenticator fails due to missing authentication data, second is executed, etc. Fallback is not used if an authenticator fails due to validation errors of the given authentication data. E.g. if an authenticator fails to validate the signature of a JWT token, the next authenticator in the list will not be executed. Instead, the entire pipeline will fail and lead to the execution of the link:{{< relref "#_error_handler_pipeline" >}}[error handler pipeline]. This list is mandatory if no link:{{< relref "default_rule.adoc" >}}[default rule] is configured.
+
NOTE: Some authenticators use the same sources to get subject authentication object from. E.g. the `jwt` and the `oauth2_introspection` authenticators can retrieve tokens from the same places in the request. If such authenticators are used in the same pipeline, you should configure the more specific ones before the more general ones to have working fallbacks. To stay with the above example, the `jwt` authenticator is more specific compared to `oauth2_introspection`, as it will be only executed, if the token is in a JWT format. In contrast to this, the `oauth2_introspection` authenticator is more general and does not care about the token format, thus will feel responsible for the request as soon as it finds a bearer token.
NOTE: Some authenticators use the same sources to get subject authentication object from. E.g. the `jwt` and the `oauth2_introspection` authenticators can retrieve tokens from the same places in the request. If such authenticators are used in the same pipeline, you should configure the more specific ones before the more general ones to have working default fallbacks. To stay with the above example, the `jwt` authenticator is more specific compared to `oauth2_introspection`, as it will be only executed, if the token is in a JWT format. In contrast to this, the `oauth2_introspection` authenticator is more general and does not care about the token format, thus will feel responsible for the request as soon as it finds a bearer token. You can however also make use of the `allow_fallback_on_error` configuration property and set it to `true`. This will allow a fallback even if the verification of the credentials fail.
* List of link:({{< relref "/docs/configuration/pipeline/hydrators.adoc" >}}[hydrators] and link:({{< relref "/docs/configuration/pipeline/authorizers.adoc" >}}[authorizers] in any order (optional). Can also be mixed. As with authenticators, the list definition happens using either `hydrator` or `authorizer` as key, followed by the required `id`. All handlers in this list are executed in the order, they are defined. If any of these fails, the entire pipeline fails, which leads to the execution of the link:{{< relref "#_error_handler_pipeline" >}}[error handler pipeline]. This list is optional.
* List link:{{< relref "/docs/configuration/pipeline/mutators.adoc" >}}[mutators] using `mutator` as key, followed by the required mutator `id`. All mutators in this list are executed in the order, they are defined. If any of these fails, the entire pipeline fails, which leads to the execution of the link:{{< relref "#_error_handler_pipeline" >}}[error handler pipeline]. This list is mandatory if no link:{{< relref "default_rule.adoc" >}}[default rule] is configured.

Expand Down
9 changes: 9 additions & 0 deletions internal/config/test_data/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pipeline:
session:
subject_attributes_from: "@this"
subject_id_from: "identity.id"
allow_fallback_on_error: true
- id: hydra_authenticator
type: oauth2_introspection
config:
Expand All @@ -134,6 +135,7 @@ pipeline:
session:
subject_attributes_from: "@this"
subject_id_from: sub
allow_fallback_on_error: true
- id: jwt_authenticator
type: jwt
config:
Expand All @@ -156,6 +158,13 @@ pipeline:
subject_attributes_from: "@this"
subject_id_from: "identity.id"
cache_ttl: 5m
allow_fallback_on_error: true
- id: basic_auth_authenticator
type: basic_auth
config:
client_id: foo
password: bar
allow_fallback_on_error: false
authorizers:
- id: allow_all_authorizer
type: allow
Expand Down
5 changes: 5 additions & 0 deletions internal/pipeline/authenticators/anonymous_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ func (a *anonymousAuthenticator) WithConfig(config map[string]any) (Authenticato

return newAnonymousAuthenticator(config)
}

func (a *anonymousAuthenticator) IsFallbackOnErrorAllowed() bool {
// not allowed, as no error can happen when this authenticator is executed
return false
}
13 changes: 13 additions & 0 deletions internal/pipeline/authenticators/anonymous_authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,16 @@ func TestAnonymousAuthenticatorExecute(t *testing.T) {
assert.Empty(t, sub.Attributes)
ctx.AssertExpectations(t)
}

func TestAnonymousAuthenticatorIsFallbackOnErrorAllowed(t *testing.T) {
t.Parallel()

// GIVEN
auth := anonymousAuthenticator{Subject: "foo"}

// WHEN
isAllowed := auth.IsFallbackOnErrorAllowed()

// THEN
require.False(t, isAllowed)
}
1 change: 1 addition & 0 deletions internal/pipeline/authenticators/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import (
type Authenticator interface {
Execute(heimdall.Context) (*subject.Subject, error)
WithConfig(config map[string]any) (Authenticator, error)
IsFallbackOnErrorAllowed() bool
}
40 changes: 25 additions & 15 deletions internal/pipeline/authenticators/basic_auth_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ func init() {
}

type basicAuthAuthenticator struct {
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
userID string
password string
allowFallbackOnError bool
}

func newBasicAuthAuthenticator(rawConfig map[string]any) (*basicAuthAuthenticator, error) {
type Config struct {
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
AllowFallbackOnError bool `mapstructure:"allow_fallback_on_error"`
}

var conf Config
Expand All @@ -64,17 +66,17 @@ func newBasicAuthAuthenticator(rawConfig map[string]any) (*basicAuthAuthenticato
NewWithMessagef(heimdall.ErrConfiguration, "basic_auth authenticator requires password to be set")
}

var auth basicAuthAuthenticator
auth := basicAuthAuthenticator{allowFallbackOnError: conf.AllowFallbackOnError}

// rewrite user id and password as hashes to mitigate potential side-channel attacks
// during credentials check
md := sha256.New()
md.Write([]byte(conf.UserID))
auth.UserID = hex.EncodeToString(md.Sum(nil))
auth.userID = hex.EncodeToString(md.Sum(nil))

md.Reset()
md.Write([]byte(conf.Password))
auth.Password = hex.EncodeToString(md.Sum(nil))
auth.password = hex.EncodeToString(md.Sum(nil))

return &auth, nil
}
Expand Down Expand Up @@ -111,8 +113,8 @@ func (a *basicAuthAuthenticator) Execute(ctx heimdall.Context) (*subject.Subject
md.Write([]byte(userIDAndPassword[1]))
password := hex.EncodeToString(md.Sum(nil))

userIDOK := userID == a.UserID
passwordOK := password == a.Password
userIDOK := userID == a.userID
passwordOK := password == a.password

if !(userIDOK && passwordOK) {
return nil, errorchain.
Expand All @@ -129,8 +131,9 @@ func (a *basicAuthAuthenticator) WithConfig(rawConfig map[string]any) (Authentic
}

type Config struct {
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
AllowFallbackOnError *bool `mapstructure:"allow_fallback_on_error"`
}

var conf Config
Expand All @@ -142,23 +145,30 @@ func (a *basicAuthAuthenticator) WithConfig(rawConfig map[string]any) (Authentic
}

return &basicAuthAuthenticator{
UserID: x.IfThenElseExec(len(conf.UserID) != 0,
userID: x.IfThenElseExec(len(conf.UserID) != 0,
func() string {
md := sha256.New()
md.Write([]byte(conf.UserID))

return hex.EncodeToString(md.Sum(nil))
}, func() string {
return a.UserID
return a.userID
}),
Password: x.IfThenElseExec(len(conf.Password) != 0,
password: x.IfThenElseExec(len(conf.Password) != 0,
func() string {
md := sha256.New()
md.Write([]byte(conf.Password))

return hex.EncodeToString(md.Sum(nil))
}, func() string {
return a.Password
return a.password
}),
allowFallbackOnError: x.IfThenElseExec(conf.AllowFallbackOnError != nil,
func() bool { return *conf.AllowFallbackOnError },
func() bool { return a.allowFallbackOnError }),
}, nil
}

func (a *basicAuthAuthenticator) IsFallbackOnErrorAllowed() bool {
return a.allowFallbackOnError
}
Loading

0 comments on commit 1336777

Please sign in to comment.