Skip to content

Commit

Permalink
feat: oauth2_authenticator updated to optionally support token sour…
Browse files Browse the repository at this point in the history
…ce selection, like specific header, schema, etc (#198)
  • Loading branch information
dadrus authored Aug 24, 2022
1 parent 488e46f commit e7ad797
Showing 7 changed files with 73 additions and 34 deletions.
9 changes: 7 additions & 2 deletions docs/content/docs/configuration/pipeline/authenticators.adoc
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ The following section describes the available authenticator types in more detail

=== Noop

As the name implies, this authenticator does nothing. It tells Heimdall to bypass the authentication. This is the only one authenticator type, which does not create a subject object on a successful execution, which is required by the most other pipeline handlers. This authenticator type also doesn't have any configuration options.
As the name implies, this authenticator does nothing. It tells Heimdall to bypass the authentication. This is the only one authenticator type, which does not create a subject object on a successful execution, which is required by the most others pipeline handlers. This authenticator type also doesn't have any configuration options.

To enable the usage of this authenticator, you have to set the `type` property to `noop`.

@@ -168,12 +168,13 @@ config:
password: super-secure
authentication_data_source:
- header: X-Custom-Bearer-Token
schema: Bearer
session:
subject_id_from: "sub"
cache_ttl: 5m
----
Usually, Bearer tokens are issued by an OAuth2 auth provider and there is a need to verify not only the validity of such, but also a couple of claims. This can be achieved by a "Local Authorizer", but there is also a special purpose [OAuth2 Introspection]({{< relref "#_oauth2_introspection">}}) authenticator type, which supports asserting all security relevant claims in just one place.
Usually, Bearer tokens are issued by an OAuth2 auth provider and there is a need to verify not only the validity of such, but also a couple of claims. This can be achieved by a link:{{< relref "/docs/configuration/pipeline/authorizers.adoc#_local" >}}[Local Authorizer], but there is also a special purpose link:{{< relref "#_oauth2_introspection">}}[OAuth2 Introspection] authenticator type, which supports asserting all security relevant claims in just one place.
====

=== OAuth2 Introspection
@@ -188,6 +189,10 @@ Configuration using the `config` property is mandatory. Following properties are
+
The introspection endpoint of the OAuth2 authorization provider. At least the `url` must be configured. There is no need to define the `method` property or setting the `Content-Type` or the `Accept` header. These are set by default to the values required by the https://datatracker.ietf.org/doc/html/rfc7662[OAuth 2.0 Token Introspection] RFC. You can however override these while configuring the authenticator.

* *`token_source`*: _link:{{< relref "configuration_types.adoc#_authentication_data_source" >}}[Authentication Data Source]_ (optional, not overridable)
+
Where to get the access token from. Defaults to retrieve it from the `Authorization` header, the `access_token` query parameter or the `access_token` body parameter (latter, if the body is of `application/x-www-form-urlencoded` MIME type).

* *`assertions`*: _link:{{< relref "configuration_types.adoc#_assertions" >}}[Assertions]_ (mandatory, overridable)
+
Configures the required claim assertions. Overriding on rule level is possible even partially. Those parts of the assertion, which have not been overridden are taken from the prototype configuration.
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ The name of the header to use.

* *`schema`*: _string_ (optional)
+
Schema, which should be present in the header value. If specified, but not present, the authentication data cannot be retrieved, effectively making the affected authenticator feel not responsible for the request.
Schema, which should be present in the header value. If specified, but not present in the header value, the authentication data cannot be retrieved, effectively making the affected authenticator feel not responsible for the request.

.Header Strategy usage
====
@@ -359,7 +359,7 @@ request_headers:
----
This condition evaluates to true only if all parts of it (`error`, `request_cidr`, `request_headers`) evaluate to true. With
* `error` evaluates to true, if the encountered error was either `precondition_error` or `authorization_error`.
* `error` evaluates to true, if the encountered error was either `precondition_error` or `authorization_error`.
* `request_cidr` evaluates to true, if the request came from an IP in either `192.168.0.0/16` or `10.0.0.0/8` range. And
* `request_headers` evaluates to true, if either the HTTP `Accept` header contains one of `text/html`, or `*/*`, or the HTTP `Contet-Type` header contains `application/json`.
====
@@ -384,9 +384,9 @@ Heimdall defines a couple of error types, which it uses to signal errors. Some o

Following types are available:

* `authentication_error` - used if an authenticator failed to verify authentication data available in the request. E.g. an authenticator was configured to verify a JWT and the signature of it was invalid.
* `authentication_error` - used if an authenticator failed to verify authentication data available in the request. E.g. an authenticator was configured to verify a JWT and the signature of it was invalid.
* `authorization_error` - used if an authorizer failed to authorize the subject. E.g. an authorizer is configured to use a script to execute on the given subject and request context, but this script returned with an error.
* `internal_error` - used if Heimdall run into an internal error condition while processing the request. E.g. something went wrong while unmarshalling a JSON object, or if there was a configuration error, which couldn't be raised while loading a rule, etc.
* `internal_error` - used if Heimdall run into an internal error condition while processing the request. E.g. something went wrong while unmarshalling a JSON object, or if there was a configuration error, which couldn't be raised while loading a rule, etc.
* `precondition_error` - used if the request does not contain required/expected data. E.g. if an authenticator could not find a cookie configured.

== Retry
@@ -540,4 +540,4 @@ This example shows how to extract the subject id from an https://www.ory.sh/docs
----
subject_id_from: identity.id
----
====
====
11 changes: 9 additions & 2 deletions docs/content/docs/configuration/reference/configuration.adoc
Original file line number Diff line number Diff line change
@@ -93,11 +93,11 @@ serve:
log:
level: debug
format: text
tracing:
service_name: heimdall
provider: jaeger
metrics:
prometheus:
host: 0.0.0.0
@@ -151,6 +151,11 @@ pipeline:
config:
user: foo
password: bar
token_source:
- header: Authorization
schema: Bearer
- query_parameter: access_token
- body_parameter: access_token
assertions:
issuers:
- http://127.0.0.1:4444/
@@ -172,6 +177,8 @@ pipeline:
jwt_from:
- header: Authorization
schema: Bearer
- query_parameter: access_token
- body_parameter: access_token
assertions:
audience:
- bla
3 changes: 3 additions & 0 deletions internal/config/test_data/test_config.yaml
Original file line number Diff line number Diff line change
@@ -130,6 +130,9 @@ pipeline:
config:
user: foo
password: bar
token_source:
- header: Authorization
schema: Bearer
assertions:
issuers:
- http://127.0.0.1:4444/
Original file line number Diff line number Diff line change
@@ -50,11 +50,12 @@ type oauth2IntrospectionAuthenticator struct {

func newOAuth2IntrospectionAuthenticator(rawConfig map[string]any) (*oauth2IntrospectionAuthenticator, error) {
type Config struct {
Endpoint endpoint.Endpoint `mapstructure:"introspection_endpoint"`
Assertions oauth2.Expectation `mapstructure:"assertions"`
Session Session `mapstructure:"session"`
CacheTTL *time.Duration `mapstructure:"cache_ttl"`
AllowFallbackOnError bool `mapstructure:"allow_fallback_on_error"`
Endpoint endpoint.Endpoint `mapstructure:"introspection_endpoint"`
AuthDataSource extractors.CompositeExtractStrategy `mapstructure:"token_source"`
Assertions oauth2.Expectation `mapstructure:"assertions"`
Session Session `mapstructure:"session"`
CacheTTL *time.Duration `mapstructure:"cache_ttl"`
AllowFallbackOnError bool `mapstructure:"allow_fallback_on_error"`
}

var conf Config
@@ -100,14 +101,19 @@ func newOAuth2IntrospectionAuthenticator(rawConfig map[string]any) (*oauth2Intro
conf.Assertions.ScopesMatcher = oauth2.NoopMatcher{}
}

extractor := extractors.CompositeExtractStrategy{
extractors.HeaderValueExtractStrategy{Name: "Authorization", Schema: "Bearer"},
extractors.QueryParameterExtractStrategy{Name: "access_token"},
extractors.BodyParameterExtractStrategy{Name: "access_token"},
}
ads := x.IfThenElseExec(conf.AuthDataSource == nil,
func() extractors.CompositeExtractStrategy {
return extractors.CompositeExtractStrategy{
extractors.HeaderValueExtractStrategy{Name: "Authorization", Schema: "Bearer"},
extractors.QueryParameterExtractStrategy{Name: "access_token"},
extractors.BodyParameterExtractStrategy{Name: "access_token"},
}
},
func() extractors.CompositeExtractStrategy { return conf.AuthDataSource },
)

return &oauth2IntrospectionAuthenticator{
ads: extractor,
ads: ads,
e: conf.Endpoint,
a: conf.Assertions,
sf: &conf.Session,
Original file line number Diff line number Diff line change
@@ -158,15 +158,30 @@ session:
},
},
{
uc: "with valid config with defaults and enabled fallback on errors",
uc: "with valid config with overwrites",
config: []byte(`
introspection_endpoint:
url: foobar.local
url: http://test.com
method: PATCH
headers:
Accept: application/foobar
token_source:
- header: foo-header
schema: foo
- query_parameter: foo_query_param
- body_parameter: foo_body_param
assertions:
scopes:
matching_strategy: wildcard
values:
- foo
issuers:
- foobar
allowed_algorithms:
- ES256
session:
subject_id_from: some_template
subject_id_from: some_claim
cache_ttl: 5s
allow_fallback_on_error: true
`),
assert: func(t *testing.T, err error, auth *oauth2IntrospectionAuthenticator) {
@@ -175,34 +190,34 @@ allow_fallback_on_error: true
require.NoError(t, err)

// assert endpoint config
assert.Equal(t, "foobar.local", auth.e.URL)
assert.Equal(t, http.MethodPost, auth.e.Method)
assert.Equal(t, "http://test.com", auth.e.URL)
assert.Equal(t, http.MethodPatch, auth.e.Method)
assert.Len(t, auth.e.Headers, 2)
assert.Contains(t, auth.e.Headers, "Content-Type")
assert.Equal(t, auth.e.Headers["Content-Type"], "application/x-www-form-urlencoded")
assert.Contains(t, auth.e.Headers, "Accept")
assert.Equal(t, auth.e.Headers["Accept"], "application/json")
assert.Equal(t, auth.e.Headers["Accept"], "application/foobar")
assert.Nil(t, auth.e.AuthStrategy)
assert.Nil(t, auth.e.Retry)

// assert assertions
assert.Len(t, auth.a.AllowedAlgorithms, len(defaultAllowedAlgorithms()))
assert.ElementsMatch(t, auth.a.AllowedAlgorithms, defaultAllowedAlgorithms())
assert.Len(t, auth.a.AllowedAlgorithms, 1)
assert.ElementsMatch(t, auth.a.AllowedAlgorithms, []string{"ES256"})
assert.Len(t, auth.a.TrustedIssuers, 1)
assert.Contains(t, auth.a.TrustedIssuers, "foobar")
assert.NoError(t, auth.a.ScopesMatcher.Match([]string{}))
assert.NoError(t, auth.a.ScopesMatcher.Match([]string{"foo"}))
assert.Equal(t, time.Duration(0), auth.a.ValidityLeeway)
assert.Empty(t, auth.a.TargetAudiences)

// assert ttl
assert.Nil(t, auth.ttl)
assert.Equal(t, 5*time.Second, *auth.ttl)

// assert token extractor settings
assert.IsType(t, extractors.CompositeExtractStrategy{}, auth.ads)
assert.Len(t, auth.ads, 3)
assert.Contains(t, auth.ads, extractors.HeaderValueExtractStrategy{Name: "Authorization", Schema: "Bearer"})
assert.Contains(t, auth.ads, extractors.QueryParameterExtractStrategy{Name: "access_token"})
assert.Contains(t, auth.ads, extractors.BodyParameterExtractStrategy{Name: "access_token"})
assert.Contains(t, auth.ads, &extractors.HeaderValueExtractStrategy{Name: "foo-header", Schema: "foo"})
assert.Contains(t, auth.ads, &extractors.QueryParameterExtractStrategy{Name: "foo_query_param"})
assert.Contains(t, auth.ads, &extractors.BodyParameterExtractStrategy{Name: "foo_body_param"})

// assert subject factory
assert.NotNil(t, auth.sf)
5 changes: 4 additions & 1 deletion schema/config.schema.json
Original file line number Diff line number Diff line change
@@ -691,6 +691,9 @@
"introspection_endpoint": {
"$ref": "#/definitions/endpointConfiguration"
},
"token_source": {
"$ref": "#/definitions/authenticationDataSource"
},
"assertions": {
"$ref": "#/definitions/assertionRequirements"
},
@@ -1559,4 +1562,4 @@
}
}
}
}
}

0 comments on commit e7ad797

Please sign in to comment.