From 9a044f9deea5bb309d8b73b93582a75f18dbb8e2 Mon Sep 17 00:00:00 2001 From: Wesley Date: Thu, 10 Dec 2020 12:17:37 +0100 Subject: [PATCH 01/10] feat: add simple bearer authenticator 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) --- .schema/config.schema.json | 111 ++++++++++ .../authenticators.simple_bearer.schema.json | 5 + driver/registry_memory.go | 1 + pipeline/authn/authenticator_simple_bearer.go | 108 ++++++++++ .../authn/authenticator_simple_bearer_test.go | 204 ++++++++++++++++++ 5 files changed, 429 insertions(+) create mode 100644 .schema/pipeline/authenticators.simple_bearer.schema.json create mode 100644 pipeline/authn/authenticator_simple_bearer.go create mode 100644 pipeline/authn/authenticator_simple_bearer_test.go diff --git a/.schema/config.schema.json b/.schema/config.schema.json index c7b76c0cd6..07084f8b6e 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -443,6 +443,85 @@ ], "additionalProperties": false }, + "configAuthenticatorsSimpleBearer": { + "type": "object", + "title": "Simple Bearer Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "check_token_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" + } + }, + "required": [ + "check_token_url" + ], + "additionalProperties": false + }, "configAuthenticatorsJwt": { "type": "object", "title": "JWT Authenticator Configuration", @@ -1232,6 +1311,38 @@ } ] }, + "simple_bearer": { + "title": "Simple Bearer", + "description": "The [`simple_bearer` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#simple_bearer).", + "type": "object", + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsSimpleBearer" + } + }, + "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).", diff --git a/.schema/pipeline/authenticators.simple_bearer.schema.json b/.schema/pipeline/authenticators.simple_bearer.schema.json new file mode 100644 index 0000000000..4cdf303e87 --- /dev/null +++ b/.schema/pipeline/authenticators.simple_bearer.schema.json @@ -0,0 +1,5 @@ +{ + "$id": "/.schema/authenticators.simple_bearer.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "/.schema/config.schema.json#/definitions/configAuthenticatorsSimpleBearer" +} diff --git a/driver/registry_memory.go b/driver/registry_memory.go index 0cc0f056db..7c5a6675ed 100644 --- a/driver/registry_memory.go +++ b/driver/registry_memory.go @@ -348,6 +348,7 @@ func (r *RegistryMemory) prepareAuthn() { interim := []authn.Authenticator{ authn.NewAuthenticatorAnonymous(r.c), authn.NewAuthenticatorCookieSession(r.c), + authn.NewAuthenticatorSimpleBearer(r.c), authn.NewAuthenticatorJWT(r.c, r), authn.NewAuthenticatorNoOp(r.c), authn.NewAuthenticatorOAuth2ClientCredentials(r.c), diff --git a/pipeline/authn/authenticator_simple_bearer.go b/pipeline/authn/authenticator_simple_bearer.go new file mode 100644 index 0000000000..1182b95b3f --- /dev/null +++ b/pipeline/authn/authenticator_simple_bearer.go @@ -0,0 +1,108 @@ +package authn + +import ( + "encoding/json" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "net/http" + + "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 AuthenticatorSimpleBearerFilter struct { +} + +type AuthenticatorSimpleBearerConfiguration struct { + CheckTokenURL string `json:"check_token_url"` + BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"` + PreservePath bool `json:"preserve_path"` + ExtraFrom string `json:"extra_from"` + SubjectFrom string `json:"subject_from"` +} + +type AuthenticatorSimpleBearer struct { + c configuration.Provider +} + +func NewAuthenticatorSimpleBearer(c configuration.Provider) *AuthenticatorSimpleBearer { + return &AuthenticatorSimpleBearer{ + c: c, + } +} + +func (a *AuthenticatorSimpleBearer) GetID() string { + return "simple_bearer" +} + +func (a *AuthenticatorSimpleBearer) Validate(config json.RawMessage) error { + if !a.c.AuthenticatorIsEnabled(a.GetID()) { + return NewErrAuthenticatorNotEnabled(a) + } + + _, err := a.Config(config) + return err +} + +func (a *AuthenticatorSimpleBearer) Config(config json.RawMessage) (*AuthenticatorSimpleBearerConfiguration, error) { + var c AuthenticatorSimpleBearerConfiguration + 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" + } + + return &c, nil +} + +func (a *AuthenticatorSimpleBearer) 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.CheckTokenURL, 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 +} diff --git a/pipeline/authn/authenticator_simple_bearer_test.go b/pipeline/authn/authenticator_simple_bearer_test.go new file mode 100644 index 0000000000..320dfc0e9b --- /dev/null +++ b/pipeline/authn/authenticator_simple_bearer_test.go @@ -0,0 +1,204 @@ +package authn_test + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/tidwall/sjson" + "io/ioutil" + "net/http" + "net/url" + "testing" + + "net/http/httptest" + + "github.com/julienschmidt/httprouter" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/oathkeeper/internal" + . "github.com/ory/oathkeeper/pipeline/authn" +) + +func TestAuthenticatorSimpleBearer(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistry(conf) + + pipelineAuthenticator, err := reg.PipelineAuthenticator("simple_bearer") + require.NoError(t, err) + + t.Run("method=authenticate", func(t *testing.T) { + for k, tc := range []struct { + d string + r *http.Request + setup func(*testing.T, *httprouter.Router) + router func(http.ResponseWriter, *http.Request) + config json.RawMessage + expectErr bool + expectExactErr error + expectSess *AuthenticationSession + }{ + { + d: "should fail because no payloads", + r: &http.Request{Header: http.Header{}}, + expectErr: true, + }, + { + d: "should return error saying that authenticator is not responsible for validating the request, as the token was not provided in a proper location (default)", + r: &http.Request{Header: http.Header{"Foobar": {"bearer token"}}}, + expectErr: true, + expectExactErr: ErrAuthenticatorNotResponsible, + }, + { + d: "should return error saying that authenticator is not responsible for validating the request, as the token was not provided in a proper location (custom header)", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, + config: []byte(`{"token_from": {"header": "X-Custom-Header"}}`), + expectErr: true, + expectExactErr: ErrAuthenticatorNotResponsible, + }, + { + d: "should fail because session store returned 400", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(400) + }) + }, + expectErr: true, + }, + { + d: "should pass because session store returned 200", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(200) + w.Write([]byte(`{"subject": "123", "extra": {"foo": "bar"}}`)) + }) + }, + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + Extra: map[string]interface{}{"foo": "bar"}, + }, + }, + { + d: "should pass through method, path, and headers to auth server", + r: &http.Request{Header: http.Header{"Authorization": {"bearer zyx"}}, URL: &url.URL{Path: "/users/123?query=string"}, Method: "PUT"}, + router: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT") + assert.Equal(t, r.URL.Path, "/users/123?query=string") + assert.Equal(t, r.Header.Get("Authorization"), "bearer zyx") + w.WriteHeader(200) + w.Write([]byte(`{"subject": "123"}`)) + }, + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + }, + }, + { + d: "should pass through method and headers ONLY to auth server when PreservePath is true", + r: &http.Request{Header: http.Header{"Authorization": {"bearer zyx"}}, URL: &url.URL{Path: "/users/123?query=string"}, Method: "PUT"}, + router: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT") + assert.Equal(t, r.URL.Path, "/") + assert.Equal(t, r.Header.Get("Authorization"), "bearer zyx") + w.WriteHeader(200) + w.Write([]byte(`{"subject": "123"}`)) + }, + config: []byte(`{"preserve_path": true}`), + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + }, + }, + { + d: "does not pass request body through to auth server", + r: &http.Request{ + Header: http.Header{ + "Authorization": {"bearer zyx"}, + "Content-Length": {"4"}, + }, + URL: &url.URL{Path: "/users/123?query=string"}, + Method: "PUT", + Body: ioutil.NopCloser(bytes.NewBufferString("body")), + }, + router: func(w http.ResponseWriter, r *http.Request) { + requestBody, _ := ioutil.ReadAll(r.Body) + assert.Equal(t, r.ContentLength, int64(0)) + assert.Equal(t, requestBody, []byte{}) + w.WriteHeader(200) + w.Write([]byte(`{"subject": "123"}`)) + }, + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + }, + }, + { + d: "should work with nested extra keys", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(200) + w.Write([]byte(`{"subject": "123", "session": {"foo": "bar"}}`)) + }) + }, + config: []byte(`{"extra_from": "session"}`), + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + Extra: map[string]interface{}{"foo": "bar"}, + }, + }, + { + d: "should work with the root key for extra and a custom subject key", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}, URL: &url.URL{Path: ""}}, + setup: func(t *testing.T, m *httprouter.Router) { + m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(200) + w.Write([]byte(`{"identity": {"id": "123"}, "session": {"foo": "bar"}}`)) + }) + }, + config: []byte(`{"subject_from": "identity.id", "extra_from": "@this"}`), + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + Extra: map[string]interface{}{"session": map[string]interface{}{"foo": "bar"}, "identity": map[string]interface{}{"id": "123"}}, + }, + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + + var ts *httptest.Server + if tc.router != nil { + ts = httptest.NewServer(http.HandlerFunc(tc.router)) + } else { + router := httprouter.New() + if tc.setup != nil { + tc.setup(t, router) + } + ts = httptest.NewServer(router) + } + defer ts.Close() + + tc.config, _ = sjson.SetBytes(tc.config, "check_token_url", ts.URL) + sess := new(AuthenticationSession) + err := pipelineAuthenticator.Authenticate(tc.r, sess, tc.config, nil) + if tc.expectErr { + require.Error(t, err) + if tc.expectExactErr != nil { + assert.EqualError(t, err, tc.expectExactErr.Error(), "%+v", err) + } + } else { + require.NoError(t, err) + } + + if tc.expectSess != nil { + assert.Equal(t, tc.expectSess, sess) + } + }) + } + }) +} \ No newline at end of file From 357cb6c4c1dae6dfdb69bf6440c9cf4047b7036a Mon Sep 17 00:00:00 2001 From: Wesley Date: Thu, 10 Dec 2020 13:18:24 +0100 Subject: [PATCH 02/10] fix: correct linting and gofmt --- pipeline/authn/authenticator_simple_bearer.go | 3 ++- pipeline/authn/authenticator_simple_bearer_test.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pipeline/authn/authenticator_simple_bearer.go b/pipeline/authn/authenticator_simple_bearer.go index 1182b95b3f..cbac2e5419 100644 --- a/pipeline/authn/authenticator_simple_bearer.go +++ b/pipeline/authn/authenticator_simple_bearer.go @@ -2,9 +2,10 @@ package authn import ( "encoding/json" + "net/http" + "github.com/pkg/errors" "github.com/tidwall/gjson" - "net/http" "github.com/ory/go-convenience/stringsx" diff --git a/pipeline/authn/authenticator_simple_bearer_test.go b/pipeline/authn/authenticator_simple_bearer_test.go index 320dfc0e9b..19f8e08cfb 100644 --- a/pipeline/authn/authenticator_simple_bearer_test.go +++ b/pipeline/authn/authenticator_simple_bearer_test.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/tidwall/sjson" "io/ioutil" "net/http" "net/url" "testing" + "github.com/tidwall/sjson" + "net/http/httptest" "github.com/julienschmidt/httprouter" @@ -201,4 +202,4 @@ func TestAuthenticatorSimpleBearer(t *testing.T) { }) } }) -} \ No newline at end of file +} From afc24ef614d75b68b2d2815b272682806eee1d92 Mon Sep 17 00:00:00 2001 From: Wesley Date: Fri, 11 Dec 2020 14:43:08 +0100 Subject: [PATCH 03/10] chore: rename simple bearer to bearer token --- .schema/config.schema.json | 12 +++++----- ...> authenticators.bearer_token.schema.json} | 4 ++-- driver/registry_memory.go | 2 +- ...earer.go => authenticator_bearer_token.go} | 22 +++++++++---------- ....go => authenticator_bearer_token_test.go} | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) rename .schema/pipeline/{authenticators.simple_bearer.schema.json => authenticators.bearer_token.schema.json} (59%) rename pipeline/authn/{authenticator_simple_bearer.go => authenticator_bearer_token.go} (76%) rename pipeline/authn/{authenticator_simple_bearer_test.go => authenticator_bearer_token_test.go} (98%) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 07084f8b6e..6f015bac83 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -443,9 +443,9 @@ ], "additionalProperties": false }, - "configAuthenticatorsSimpleBearer": { + "configAuthenticatorsBearerToken": { "type": "object", - "title": "Simple Bearer Authenticator Configuration", + "title": "Bearer Token Authenticator Configuration", "description": "This section is optional when the authenticator is disabled.", "properties": { "check_token_url": { @@ -1311,9 +1311,9 @@ } ] }, - "simple_bearer": { - "title": "Simple Bearer", - "description": "The [`simple_bearer` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#simple_bearer).", + "bearer_token": { + "title": "Bearer Token", + "description": "The [`bearer_token` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#bearer_token).", "type": "object", "properties": { "enabled": { @@ -1327,7 +1327,7 @@ "const": true }, "config": { - "$ref": "#/definitions/configAuthenticatorsSimpleBearer" + "$ref": "#/definitions/configAuthenticatorsBearerToken" } }, "required": [ diff --git a/.schema/pipeline/authenticators.simple_bearer.schema.json b/.schema/pipeline/authenticators.bearer_token.schema.json similarity index 59% rename from .schema/pipeline/authenticators.simple_bearer.schema.json rename to .schema/pipeline/authenticators.bearer_token.schema.json index 4cdf303e87..54a9cdfcbb 100644 --- a/.schema/pipeline/authenticators.simple_bearer.schema.json +++ b/.schema/pipeline/authenticators.bearer_token.schema.json @@ -1,5 +1,5 @@ { - "$id": "/.schema/authenticators.simple_bearer.schema.json", + "$id": "/.schema/authenticators.bearer_token.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "/.schema/config.schema.json#/definitions/configAuthenticatorsSimpleBearer" + "$ref": "/.schema/config.schema.json#/definitions/configAuthenticatorsBearerToken" } diff --git a/driver/registry_memory.go b/driver/registry_memory.go index 7c5a6675ed..a4c9c8c012 100644 --- a/driver/registry_memory.go +++ b/driver/registry_memory.go @@ -348,7 +348,7 @@ func (r *RegistryMemory) prepareAuthn() { interim := []authn.Authenticator{ authn.NewAuthenticatorAnonymous(r.c), authn.NewAuthenticatorCookieSession(r.c), - authn.NewAuthenticatorSimpleBearer(r.c), + authn.NewAuthenticatorBearerToken(r.c), authn.NewAuthenticatorJWT(r.c, r), authn.NewAuthenticatorNoOp(r.c), authn.NewAuthenticatorOAuth2ClientCredentials(r.c), diff --git a/pipeline/authn/authenticator_simple_bearer.go b/pipeline/authn/authenticator_bearer_token.go similarity index 76% rename from pipeline/authn/authenticator_simple_bearer.go rename to pipeline/authn/authenticator_bearer_token.go index cbac2e5419..61b4c1d814 100644 --- a/pipeline/authn/authenticator_simple_bearer.go +++ b/pipeline/authn/authenticator_bearer_token.go @@ -20,10 +20,10 @@ func init() { }) } -type AuthenticatorSimpleBearerFilter struct { +type AuthenticatorBearerTokenFilter struct { } -type AuthenticatorSimpleBearerConfiguration struct { +type AuthenticatorBearerTokenConfiguration struct { CheckTokenURL string `json:"check_token_url"` BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"` PreservePath bool `json:"preserve_path"` @@ -31,21 +31,21 @@ type AuthenticatorSimpleBearerConfiguration struct { SubjectFrom string `json:"subject_from"` } -type AuthenticatorSimpleBearer struct { +type AuthenticatorBearerToken struct { c configuration.Provider } -func NewAuthenticatorSimpleBearer(c configuration.Provider) *AuthenticatorSimpleBearer { - return &AuthenticatorSimpleBearer{ +func NewAuthenticatorBearerToken(c configuration.Provider) *AuthenticatorBearerToken { + return &AuthenticatorBearerToken{ c: c, } } -func (a *AuthenticatorSimpleBearer) GetID() string { - return "simple_bearer" +func (a *AuthenticatorBearerToken) GetID() string { + return "bearer_token" } -func (a *AuthenticatorSimpleBearer) Validate(config json.RawMessage) error { +func (a *AuthenticatorBearerToken) Validate(config json.RawMessage) error { if !a.c.AuthenticatorIsEnabled(a.GetID()) { return NewErrAuthenticatorNotEnabled(a) } @@ -54,8 +54,8 @@ func (a *AuthenticatorSimpleBearer) Validate(config json.RawMessage) error { return err } -func (a *AuthenticatorSimpleBearer) Config(config json.RawMessage) (*AuthenticatorSimpleBearerConfiguration, error) { - var c AuthenticatorSimpleBearerConfiguration +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) } @@ -71,7 +71,7 @@ func (a *AuthenticatorSimpleBearer) Config(config json.RawMessage) (*Authenticat return &c, nil } -func (a *AuthenticatorSimpleBearer) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error { +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 diff --git a/pipeline/authn/authenticator_simple_bearer_test.go b/pipeline/authn/authenticator_bearer_token_test.go similarity index 98% rename from pipeline/authn/authenticator_simple_bearer_test.go rename to pipeline/authn/authenticator_bearer_token_test.go index 19f8e08cfb..f0e92cd5d0 100644 --- a/pipeline/authn/authenticator_simple_bearer_test.go +++ b/pipeline/authn/authenticator_bearer_token_test.go @@ -22,11 +22,11 @@ import ( . "github.com/ory/oathkeeper/pipeline/authn" ) -func TestAuthenticatorSimpleBearer(t *testing.T) { +func TestAuthenticatorBearerToken(t *testing.T) { conf := internal.NewConfigurationWithDefaults() reg := internal.NewRegistry(conf) - pipelineAuthenticator, err := reg.PipelineAuthenticator("simple_bearer") + pipelineAuthenticator, err := reg.PipelineAuthenticator("bearer_token") require.NoError(t, err) t.Run("method=authenticate", func(t *testing.T) { From 9206906aa0121387f3215d5ce3dceb8dd5c5f700 Mon Sep 17 00:00:00 2001 From: Wesley Date: Fri, 11 Dec 2020 15:00:39 +0100 Subject: [PATCH 04/10] chore: rename check_token_url to check_session_url So it matches the cookie_session authenticator better as functionality is the same: a session store. --- pipeline/authn/authenticator_bearer_token.go | 4 ++-- pipeline/authn/authenticator_bearer_token_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pipeline/authn/authenticator_bearer_token.go b/pipeline/authn/authenticator_bearer_token.go index 61b4c1d814..650a37a08b 100644 --- a/pipeline/authn/authenticator_bearer_token.go +++ b/pipeline/authn/authenticator_bearer_token.go @@ -24,7 +24,7 @@ type AuthenticatorBearerTokenFilter struct { } type AuthenticatorBearerTokenConfiguration struct { - CheckTokenURL string `json:"check_token_url"` + CheckSessionURL string `json:"check_session_url"` BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"` PreservePath bool `json:"preserve_path"` ExtraFrom string `json:"extra_from"` @@ -82,7 +82,7 @@ func (a *AuthenticatorBearerToken) Authenticate(r *http.Request, session *Authen return errors.WithStack(ErrAuthenticatorNotResponsible) } - body, err := forwardRequestToSessionStore(r, cf.CheckTokenURL, cf.PreservePath) + body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath) if err != nil { return err } diff --git a/pipeline/authn/authenticator_bearer_token_test.go b/pipeline/authn/authenticator_bearer_token_test.go index f0e92cd5d0..09ecd72a66 100644 --- a/pipeline/authn/authenticator_bearer_token_test.go +++ b/pipeline/authn/authenticator_bearer_token_test.go @@ -184,7 +184,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { } defer ts.Close() - tc.config, _ = sjson.SetBytes(tc.config, "check_token_url", ts.URL) + tc.config, _ = sjson.SetBytes(tc.config, "check_session_url", ts.URL) sess := new(AuthenticationSession) err := pipelineAuthenticator.Authenticate(tc.r, sess, tc.config, nil) if tc.expectErr { From 72f502006eec087d95ca30a7e5d64e531c339a19 Mon Sep 17 00:00:00 2001 From: Wesley Date: Fri, 11 Dec 2020 15:01:34 +0100 Subject: [PATCH 05/10] docs: add bearer_token authenticator to the docs --- docs/docs/pipeline/authn.md | 123 ++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/docs/docs/pipeline/authn.md b/docs/docs/pipeline/authn.md index 3fa2d6e46e..370a4853c9 100644 --- a/docs/docs/pipeline/authn.md +++ b/docs/docs/pipeline/authn.md @@ -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 + [GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing + to the `subject` field. This defaults to `subject`. 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 From 4c4b6431334bd2afb44ac34fbd9b1b1b4b2fc269 Mon Sep 17 00:00:00 2001 From: Wesley Date: Fri, 11 Dec 2020 15:03:28 +0100 Subject: [PATCH 06/10] fix: rename check_token_url to check_session_url in schema --- .schema/config.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 6f015bac83..6bca77486b 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -448,7 +448,7 @@ "title": "Bearer Token Authenticator Configuration", "description": "This section is optional when the authenticator is disabled.", "properties": { - "check_token_url": { + "check_session_url": { "title": "Token Check URL", "type": "string", "format": "uri", @@ -518,7 +518,7 @@ } }, "required": [ - "check_token_url" + "check_session_url" ], "additionalProperties": false }, From b08496d286b51a52c69067f1b7932bac2edcd49f Mon Sep 17 00:00:00 2001 From: Wesley Date: Tue, 15 Dec 2020 12:16:42 +0100 Subject: [PATCH 07/10] chore: rename default subject_from to 'sub' --- docs/docs/pipeline/authn.md | 2 +- pipeline/authn/authenticator_bearer_token.go | 2 +- pipeline/authn/authenticator_bearer_token_test.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/docs/pipeline/authn.md b/docs/docs/pipeline/authn.md index 370a4853c9..977a1818cf 100644 --- a/docs/docs/pipeline/authn.md +++ b/docs/docs/pipeline/authn.md @@ -349,7 +349,7 @@ appropriately. `{ "subject": "...", "session": { "foo": {"bar": "whatever"} } }`, and so on. - `subject_from` (string, optional - defaults to `subject`) - A [GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing - to the `subject` field. This defaults to `subject`. Example: `identity.id` for + to the `subject` 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 - diff --git a/pipeline/authn/authenticator_bearer_token.go b/pipeline/authn/authenticator_bearer_token.go index 650a37a08b..5385863fa0 100644 --- a/pipeline/authn/authenticator_bearer_token.go +++ b/pipeline/authn/authenticator_bearer_token.go @@ -65,7 +65,7 @@ func (a *AuthenticatorBearerToken) Config(config json.RawMessage) (*Authenticato } if len(c.SubjectFrom) == 0 { - c.SubjectFrom = "subject" + c.SubjectFrom = "sub" } return &c, nil diff --git a/pipeline/authn/authenticator_bearer_token_test.go b/pipeline/authn/authenticator_bearer_token_test.go index 09ecd72a66..e96ea4deaf 100644 --- a/pipeline/authn/authenticator_bearer_token_test.go +++ b/pipeline/authn/authenticator_bearer_token_test.go @@ -74,7 +74,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { setup: func(t *testing.T, m *httprouter.Router) { m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { w.WriteHeader(200) - w.Write([]byte(`{"subject": "123", "extra": {"foo": "bar"}}`)) + w.Write([]byte(`{"sub": "123", "extra": {"foo": "bar"}}`)) }) }, expectErr: false, @@ -91,7 +91,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { assert.Equal(t, r.URL.Path, "/users/123?query=string") assert.Equal(t, r.Header.Get("Authorization"), "bearer zyx") w.WriteHeader(200) - w.Write([]byte(`{"subject": "123"}`)) + w.Write([]byte(`{"sub": "123"}`)) }, expectErr: false, expectSess: &AuthenticationSession{ @@ -106,7 +106,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { assert.Equal(t, r.URL.Path, "/") assert.Equal(t, r.Header.Get("Authorization"), "bearer zyx") w.WriteHeader(200) - w.Write([]byte(`{"subject": "123"}`)) + w.Write([]byte(`{"sub": "123"}`)) }, config: []byte(`{"preserve_path": true}`), expectErr: false, @@ -130,7 +130,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { assert.Equal(t, r.ContentLength, int64(0)) assert.Equal(t, requestBody, []byte{}) w.WriteHeader(200) - w.Write([]byte(`{"subject": "123"}`)) + w.Write([]byte(`{"sub": "123"}`)) }, expectErr: false, expectSess: &AuthenticationSession{ @@ -143,7 +143,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { setup: func(t *testing.T, m *httprouter.Router) { m.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { w.WriteHeader(200) - w.Write([]byte(`{"subject": "123", "session": {"foo": "bar"}}`)) + w.Write([]byte(`{"sub": "123", "session": {"foo": "bar"}}`)) }) }, config: []byte(`{"extra_from": "session"}`), From 092c021d6f5d0f92db8f7388b9ddcb595cdd82ea Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 15 Dec 2020 13:34:05 +0100 Subject: [PATCH 08/10] Update docs/docs/pipeline/authn.md --- docs/docs/pipeline/authn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/pipeline/authn.md b/docs/docs/pipeline/authn.md index 977a1818cf..e968cd00a5 100644 --- a/docs/docs/pipeline/authn.md +++ b/docs/docs/pipeline/authn.md @@ -347,7 +347,7 @@ appropriately. 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 +- `subject_from` (string, optional - defaults to `sub`) - A [GJSON Path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) pointing to the `subject` field. This defaults to `sub`. Example: `identity.id` for `{ "identity": { "id": "1234" } }`. From 73da09cf8e5844b00cfdec1a89796b492743971d Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 15 Dec 2020 13:34:11 +0100 Subject: [PATCH 09/10] Update .schema/config.schema.json --- .schema/config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 6bca77486b..d550053ff3 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -514,7 +514,7 @@ "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" + "default": "sub" } }, "required": [ From 5e11618576002cc4601878531692cb866daca4c7 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Tue, 15 Dec 2020 13:34:54 +0100 Subject: [PATCH 10/10] Update docs/docs/pipeline/authn.md --- docs/docs/pipeline/authn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/pipeline/authn.md b/docs/docs/pipeline/authn.md index e968cd00a5..10e66bcc7e 100644 --- a/docs/docs/pipeline/authn.md +++ b/docs/docs/pipeline/authn.md @@ -349,7 +349,7 @@ appropriately. `{ "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 `subject` field. This defaults to `sub`. Example: `identity.id` for + 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 -