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

feat: add bearer_token authenticator #613

Merged
merged 10 commits into from
Dec 15, 2020
111 changes: 111 additions & 0 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,85 @@
],
"additionalProperties": false
},
"configAuthenticatorsBearerToken": {
"type": "object",
"title": "Bearer Token Authenticator Configuration",
"description": "This section is optional when the authenticator is disabled.",
"properties": {
"check_session_url": {
"title": "Token Check URL",
"type": "string",
"format": "uri",
"description": "The origin to proxy requests to. If the response is a 200 with body `{ \"subject\": \"...\", \"extra\": {} }`. The request will pass the subject through successfully, otherwise it will be marked as unauthorized.\n\n>If this authenticator is enabled, this value is required.",
"examples": [
"https://session-store-host"
]
},
"token_from": {
"title": "Token From",
"description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.",
"oneOf": [
{
"type": "null"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"header": {
"title": "Header",
"type": "string",
"description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter or cookie."
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"query_parameter": {
"title": "Query Parameter",
"type": "string",
"description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header or cookie."
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"cookie": {
"title": "Cookie",
"type": "string",
"description": "The cookie (case sensitive) that must contain a token for request authentication.\n It can't be set along with header or query_parameter."
}
}
}
]
},
"preserve_path": {
"title": "Preserve Path",
"type": "boolean",
"description": "When set to true, any path specified in `check_session_url` will be preserved instead of overwriting the path with the path from the original request"
},
"extra_from": {
"title": "Extra JSON Path",
"description": "The `extra` field in the ORY Oathkeeper authentication session is set using this JSON Path. Defaults to `extra`, and could be `@this` (for the root element), `foo.bar` (for key foo.bar), or any other valid GJSON path. See [GSJON Syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for reference.",
"type": "string",
"default": "extra"
},
"subject_from": {
"title": "Subject JSON Path",
"description": "The `subject` field in the ORY Oathkeeper authentication session is set using this JSON Path. Defaults to `subject`. See [GSJON Syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for reference.",
"type": "string",
"default": "subject"
aeneasr marked this conversation as resolved.
Show resolved Hide resolved
}
},
"required": [
"check_session_url"
],
"additionalProperties": false
},
"configAuthenticatorsJwt": {
"type": "object",
"title": "JWT Authenticator Configuration",
Expand Down Expand Up @@ -1232,6 +1311,38 @@
}
]
},
"bearer_token": {
"title": "Bearer Token",
"description": "The [`bearer_token` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#bearer_token).",
"type": "object",
"properties": {
"enabled": {
"$ref": "#/definitions/handlerSwitch"
}
},
"oneOf": [
{
"properties": {
"enabled": {
"const": true
},
"config": {
"$ref": "#/definitions/configAuthenticatorsBearerToken"
}
},
"required": [
"config"
]
},
{
"properties": {
"enabled": {
"const": false
}
}
}
]
},
"jwt": {
"title": "JSON Web Token (jwt)",
"description": "The [`jwt` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#jwt).",
Expand Down
5 changes: 5 additions & 0 deletions .schema/pipeline/authenticators.bearer_token.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$id": "/.schema/authenticators.bearer_token.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "/.schema/config.schema.json#/definitions/configAuthenticatorsBearerToken"
}
123 changes: 123 additions & 0 deletions docs/docs/pipeline/authn.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,129 @@ HTTP/1.0 401 Status Unauthorized
The request is not authorized because the provided credentials are invalid.
```

## `bearer_token`

The `bearer_token` authenticator will forward the request method, path and
headers to a session store. If the session store returns `200 OK` and body
`{ "subject": "...", "extra": {} }` then the authenticator will set the subject
appropriately.

### Configuration

- `check_session_url` (string, required) - The session store to forward request
method/path/headers to for validation.
- `preserve_path` (boolean, optional) - If set, any path in `check_session_url`
will be preserved instead of replacing the path with the path of the request
being checked.
- `extra_from` (string, optional - defaults to `extra`) - A
[GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing
to the `extra` field. This defaults to `extra`, but it could also be `@this`
(for the root element), `session.foo.bar` for
`{ "subject": "...", "session": { "foo": {"bar": "whatever"} } }`, and so on.
- `subject_from` (string, optional - defaults to `subject`) - A
aeneasr marked this conversation as resolved.
Show resolved Hide resolved
[GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing
to the `subject` field. This defaults to `subject`. Example: `identity.id` for
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to the `subject` field. This defaults to `subject`. Example: `identity.id` for
to the `subject` field. This defaults to `sub`. Example: `identity.id` for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just sent in the change. This does make it diverge from the cookie_session configuration, but perhaps that will get the change sometime in the future as well?

`{ "identity": { "id": "1234" } }`.
- `token_from` (object, optional) - The location of the bearer token. If not
configured, the token will be received from a default location -
'Authorization' header. One and only one location (header, query, or cookie)
must be specified.
- `header` (string, required, one of) - The header (case insensitive) that
must contain a Bearer token for request authentication. It can't be set
along with `query_parameter` or `cookie`.
- `query_parameter` (string, required, one of) - The query parameter (case
sensitive) that must contain a Bearer token for request authentication. It
can't be set along with `header` or `cookie`.
- `cookie` (string, required, one of) - The cookie (case sensitive) that must
contain a Bearer token for request authentication. It can't be set along
with `header` or `query_parameter`

```yaml
# Global configuration file oathkeeper.yml
authenticators:
bearer_token:
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
enabled: true

config:
check_session_url: https://session-store-host
token_from:
header: Custom-Authorization-Header
# or
# query_parameter: auth-token
# or
# cookie: auth-token
```

```yaml
# Some Access Rule: access-rule-1.yaml
id: access-rule-1
# match: ...
# upstream: ...
authenticators:
- handler: bearer_token
config:
check_session_url: https://session-store-host
token_from:
query_parameter: auth-token
# or
# header: Custom-Authorization-Header
# or
# cookie: auth-token
```

```yaml
# Some Access Rule Preserving Path: access-rule-2.yaml
id: access-rule-2
# match: ...
# upstream: ...
authenticators:
- handler: bearer_token
config:
check_session_url: https://session-store-host/check-session
token_from:
query_parameter: auth-token
# or
# header: Custom-Authorization-Header
# or
# cookie: auth-token
preserve_path: true
```

### Access Rule Example

```shell
$ cat ./rules.json

[{
"id": "some-id",
"upstream": {
"url": "http://my-backend-service"
},
"match": {
"url": "http://my-app/some-route",
"methods": [
"GET"
]
},
"authenticators": [{
"handler": "bearer_token"
}],
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
}]

$ curl -X GET -H 'Authorization: Bearer valid-token' http://my-app/some-route

HTTP/1.0 200 OK
The request has been allowed! The subject is: "peter"

$ curl -X GET -H 'Authorization: Bearer invalid-token' http://my-app/some-route

HTTP/1.0 401 Status Unauthorized
The request is not authorized because the provided credentials are invalid.
```

## `oauth2_client_credentials`

This `oauth2_client_credentials` uses the username and password from HTTP Basic
Expand Down
1 change: 1 addition & 0 deletions driver/registry_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ func (r *RegistryMemory) prepareAuthn() {
interim := []authn.Authenticator{
authn.NewAuthenticatorAnonymous(r.c),
authn.NewAuthenticatorCookieSession(r.c),
authn.NewAuthenticatorBearerToken(r.c),
authn.NewAuthenticatorJWT(r.c, r),
authn.NewAuthenticatorNoOp(r.c),
authn.NewAuthenticatorOAuth2ClientCredentials(r.c),
Expand Down
109 changes: 109 additions & 0 deletions pipeline/authn/authenticator_bearer_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package authn

import (
"encoding/json"
"net/http"

"github.com/pkg/errors"
"github.com/tidwall/gjson"

"github.com/ory/go-convenience/stringsx"

"github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/helper"
"github.com/ory/oathkeeper/pipeline"
)

func init() {
gjson.AddModifier("this", func(json, arg string) string {
return json
})
}

type AuthenticatorBearerTokenFilter struct {
}

type AuthenticatorBearerTokenConfiguration struct {
CheckSessionURL string `json:"check_session_url"`
BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"`
PreservePath bool `json:"preserve_path"`
ExtraFrom string `json:"extra_from"`
SubjectFrom string `json:"subject_from"`
}

type AuthenticatorBearerToken struct {
c configuration.Provider
}

func NewAuthenticatorBearerToken(c configuration.Provider) *AuthenticatorBearerToken {
return &AuthenticatorBearerToken{
c: c,
}
}

func (a *AuthenticatorBearerToken) GetID() string {
return "bearer_token"
}

func (a *AuthenticatorBearerToken) Validate(config json.RawMessage) error {
if !a.c.AuthenticatorIsEnabled(a.GetID()) {
return NewErrAuthenticatorNotEnabled(a)
}

_, err := a.Config(config)
return err
}

func (a *AuthenticatorBearerToken) Config(config json.RawMessage) (*AuthenticatorBearerTokenConfiguration, error) {
var c AuthenticatorBearerTokenConfiguration
if err := a.c.AuthenticatorConfig(a.GetID(), config, &c); err != nil {
return nil, NewErrAuthenticatorMisconfigured(a, err)
}

if len(c.ExtraFrom) == 0 {
c.ExtraFrom = "extra"
}

if len(c.SubjectFrom) == 0 {
c.SubjectFrom = "subject"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth2 uses the shorthand sub - I think we should use sub per default also:

Suggested change
c.SubjectFrom = "subject"
c.SubjectFrom = "sub"

}

return &c, nil
}

func (a *AuthenticatorBearerToken) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
cf, err := a.Config(config)
if err != nil {
return err
}

token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
if token == "" {
return errors.WithStack(ErrAuthenticatorNotResponsible)
}

body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath)
if err != nil {
return err
}

var (
subject string
extra map[string]interface{}

subjectRaw = []byte(stringsx.Coalesce(gjson.GetBytes(body, cf.SubjectFrom).Raw, "null"))
extraRaw = []byte(stringsx.Coalesce(gjson.GetBytes(body, cf.ExtraFrom).Raw, "null"))
)

if err = json.Unmarshal(subjectRaw, &subject); err != nil {
return helper.ErrForbidden.WithReasonf("The configured subject_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.SubjectFrom, body, subjectRaw).WithTrace(err)
}

if err = json.Unmarshal(extraRaw, &extra); err != nil {
return helper.ErrForbidden.WithReasonf("The configured extra_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.ExtraFrom, body, extraRaw).WithTrace(err)
}

session.Subject = subject
session.Extra = extra
return nil
}
Loading