Skip to content

Commit

Permalink
feat: add bearer_token authenticator (#613)
Browse files Browse the repository at this point in the history
Adds a new authenticator to work with Kratos' new API token.
Works the same as the cookie_session authenticator but checks for a
bearer token in the Authorization header (unless overwritten by
token_from)
  • Loading branch information
wezzle authored Dec 15, 2020
1 parent 0116e70 commit b623ae7
Show file tree
Hide file tree
Showing 6 changed files with 554 additions and 0 deletions.
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": "sub"
}
},
"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 `sub`) - A
[GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing
to the `sub` field. This defaults to `sub`. Example: `identity.id` for
`{ "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 = "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

0 comments on commit b623ae7

Please sign in to comment.