From 32e241bccc4be71031860f5c599f9c3fada81c20 Mon Sep 17 00:00:00 2001 From: viters Date: Wed, 21 Dec 2022 19:36:24 +0100 Subject: [PATCH 1/2] feat: basic authenticator (#254) --- .schemas/authenticators.basic.schema.json | 16 ++++ .schemas/config.schema.json | 25 +++++ driver/registry_memory.go | 1 + pipeline/authn/authenticator_basic.go | 95 +++++++++++++++++++ pipeline/authn/authenticator_basic_test.go | 68 +++++++++++++ spec/config.schema.json | 25 +++++ .../pipeline/authenticators.basic.schema.json | 5 + 7 files changed, 235 insertions(+) create mode 100644 .schemas/authenticators.basic.schema.json create mode 100644 pipeline/authn/authenticator_basic.go create mode 100644 pipeline/authn/authenticator_basic_test.go create mode 100644 spec/pipeline/authenticators.basic.schema.json diff --git a/.schemas/authenticators.basic.schema.json b/.schemas/authenticators.basic.schema.json new file mode 100644 index 0000000000..f90672f808 --- /dev/null +++ b/.schemas/authenticators.basic.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/oathkeeper/master/.schemas/authenticators.basic.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Basic Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "credentials": { + "title": "Credentials", + "type": "string", + "description": "The Basic credentials in form of 'username:password' hashed with SHA256" + } + }, + "required": ["credentials"], + "additionalProperties": false +} diff --git a/.schemas/config.schema.json b/.schemas/config.schema.json index 216788c8cd..9814f56b87 100644 --- a/.schemas/config.schema.json +++ b/.schemas/config.schema.json @@ -310,6 +310,20 @@ }, "additionalProperties": false }, + "configAuthenticatorsBasic": { + "type": "object", + "title": "Basic Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "credentials": { + "title": "Credentials", + "type": "string", + "description": "The Basic credentials in form of 'username:password' hashed with SHA256" + } + }, + "required": ["credentials"], + "additionalProperties": false + }, "configAuthenticatorsCookieSession": { "type": "object", "title": "Cookie Session Authenticator Configuration", @@ -998,6 +1012,17 @@ } } }, + "basic": { + "title": "Basic", + "description": "The [`basic` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#basic).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/configAuthenticatorsBasic" + } + } + }, "noop": { "title": "No Operation (noop)", "description": "The [`noop` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#noop).", diff --git a/driver/registry_memory.go b/driver/registry_memory.go index 259271dd42..251ec0a614 100644 --- a/driver/registry_memory.go +++ b/driver/registry_memory.go @@ -369,6 +369,7 @@ func (r *RegistryMemory) prepareAuthn() { interim := []authn.Authenticator{ authn.NewAuthenticatorAnonymous(r.c), authn.NewAuthenticatorCookieSession(r.c), + authn.NewAuthenticatorBasic(r.c), authn.NewAuthenticatorBearerToken(r.c), authn.NewAuthenticatorJWT(r.c, r), authn.NewAuthenticatorNoOp(r.c), diff --git a/pipeline/authn/authenticator_basic.go b/pipeline/authn/authenticator_basic.go new file mode 100644 index 0000000000..6c2f92f48e --- /dev/null +++ b/pipeline/authn/authenticator_basic.go @@ -0,0 +1,95 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package authn + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/http" + "strings" + + "github.com/ory/oathkeeper/driver/configuration" + "github.com/ory/oathkeeper/helper" + "github.com/ory/oathkeeper/pipeline" +) + +type AuthenticatorBasicConfiguration struct { + Credentials string `json:"credentials"` +} + +type AuthenticatorBasic struct { + c configuration.Provider +} + +func NewAuthenticatorBasic( + c configuration.Provider, +) *AuthenticatorBasic { + return &AuthenticatorBasic{ + c: c, + } +} + +func (a *AuthenticatorBasic) GetID() string { + return "basic" +} + +func (a *AuthenticatorBasic) Validate(config json.RawMessage) error { + if !a.c.AuthenticatorIsEnabled(a.GetID()) { + return NewErrAuthenticatorNotEnabled(a) + } + + _, err := a.Config(config) + return err +} + +func (a *AuthenticatorBasic) Config(config json.RawMessage) (*AuthenticatorBasicConfiguration, error) { + var c AuthenticatorBasicConfiguration + if err := a.c.AuthenticatorConfig(a.GetID(), config, &c); err != nil { + return nil, NewErrAuthenticatorMisconfigured(a, err) + } + + return &c, nil +} + +func (a *AuthenticatorBasic) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error { + cf, err := a.Config(config) + if err != nil { + return err + } + + authorization := r.Header.Get("Authorization") + if authorization == "" { + return helper.ErrUnauthorized + } + + token, err := BasicTokenFromHeader(authorization) + if err != nil { + return helper.ErrUnauthorized.WithReason("Basic token is not correctly base64 encoded") + } + + h := sha256.New() + h.Write([]byte(token)) + hash := hex.EncodeToString(h.Sum(nil)) + + if hash == cf.Credentials { + return nil + } + + return helper.ErrUnauthorized +} + +func BasicTokenFromHeader(header string) (string, error) { + split := strings.SplitN(header, " ", 2) + if len(split) != 2 || !strings.EqualFold(strings.ToLower(split[0]), "basic") { + return "", nil + } + rawDecodedText, err := base64.StdEncoding.DecodeString(split[1]) + if err != nil { + return "", err + } + + return string(rawDecodedText), nil +} diff --git a/pipeline/authn/authenticator_basic_test.go b/pipeline/authn/authenticator_basic_test.go new file mode 100644 index 0000000000..10b9ce8710 --- /dev/null +++ b/pipeline/authn/authenticator_basic_test.go @@ -0,0 +1,68 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package authn_test + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/ory/oathkeeper/helper" + "github.com/ory/oathkeeper/internal" + "github.com/ory/oathkeeper/pipeline/authn" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAuthenticatorBasic(t *testing.T) { + t.Parallel() + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistry(conf) + + session := new(authn.AuthenticationSession) + + a, err := reg.PipelineAuthenticator("basic") + require.NoError(t, err) + assert.Equal(t, "basic", a.GetID()) + + t.Run("method=authenticate/case=empty auth", func(t *testing.T) { + err := a.Authenticate( + &http.Request{Header: http.Header{}}, + session, + json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), + nil) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + }) + + t.Run("method=authenticate/case=incorrect base64 token", func(t *testing.T) { + err := a.Authenticate( + &http.Request{Header: http.Header{ "Authorization": {"Basic " + "123"} }}, + session, + json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), + nil) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + }) + + t.Run("method=authenticate/case=incorrect auth", func(t *testing.T) { + err := a.Authenticate( + &http.Request{Header: http.Header{ "Authorization": {"Basic " + "dXNlcjpwYXNz"} }}, + session, + json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), + nil) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + }) + + t.Run("method=authenticate/case=correct auth", func(t *testing.T) { + err := a.Authenticate( + &http.Request{Header: http.Header{ "Authorization": {"Basic " + "dXNlcm5hbWU6cGFzc3dvcmQ="} }}, + session, + json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), + nil) + require.NoError(t, err) + }) +} diff --git a/spec/config.schema.json b/spec/config.schema.json index 2409c777f5..c86f677f43 100644 --- a/spec/config.schema.json +++ b/spec/config.schema.json @@ -354,6 +354,20 @@ }, "additionalProperties": false }, + "configAuthenticatorsBasic": { + "type": "object", + "title": "Basic Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "credentials": { + "title": "Credentials", + "type": "string", + "description": "The Basic credentials in form of 'username:password' hashed with SHA256" + } + }, + "required": ["credentials"], + "additionalProperties": false + }, "configAuthenticatorsCookieSession": { "type": "object", "title": "Cookie Session Authenticator Configuration", @@ -1290,6 +1304,17 @@ } } }, + "basic": { + "title": "Basic", + "description": "The [`basic` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#basic).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/configAuthenticatorsBasic" + } + } + }, "noop": { "title": "No Operation (noop)", "description": "The [`noop` authenticator](https://www.ory.sh/oathkeeper/docs/pipeline/authn#noop).", diff --git a/spec/pipeline/authenticators.basic.schema.json b/spec/pipeline/authenticators.basic.schema.json new file mode 100644 index 0000000000..95c1cd1734 --- /dev/null +++ b/spec/pipeline/authenticators.basic.schema.json @@ -0,0 +1,5 @@ +{ + "$id": "/.schema/authenticators.basic.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "/.schema/config.schema.json#/definitions/configAuthenticatorsBasic" +} From d1b382cd41c20b358395e3887968a46c7ad19942 Mon Sep 17 00:00:00 2001 From: viters Date: Wed, 21 Dec 2022 20:48:51 +0100 Subject: [PATCH 2/2] fix: goimports --- pipeline/authn/authenticator_basic_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pipeline/authn/authenticator_basic_test.go b/pipeline/authn/authenticator_basic_test.go index 10b9ce8710..d9ebf39043 100644 --- a/pipeline/authn/authenticator_basic_test.go +++ b/pipeline/authn/authenticator_basic_test.go @@ -33,36 +33,36 @@ func TestAuthenticatorBasic(t *testing.T) { session, json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), nil) - require.Error(t, err) - assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) }) t.Run("method=authenticate/case=incorrect base64 token", func(t *testing.T) { err := a.Authenticate( - &http.Request{Header: http.Header{ "Authorization": {"Basic " + "123"} }}, + &http.Request{Header: http.Header{"Authorization": {"Basic " + "123"}}}, session, json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), nil) - require.Error(t, err) - assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) }) t.Run("method=authenticate/case=incorrect auth", func(t *testing.T) { err := a.Authenticate( - &http.Request{Header: http.Header{ "Authorization": {"Basic " + "dXNlcjpwYXNz"} }}, + &http.Request{Header: http.Header{"Authorization": {"Basic " + "dXNlcjpwYXNz"}}}, session, json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), nil) - require.Error(t, err) - assert.EqualError(t, err, helper.ErrUnauthorized.Error()) + require.Error(t, err) + assert.EqualError(t, err, helper.ErrUnauthorized.Error()) }) t.Run("method=authenticate/case=correct auth", func(t *testing.T) { err := a.Authenticate( - &http.Request{Header: http.Header{ "Authorization": {"Basic " + "dXNlcm5hbWU6cGFzc3dvcmQ="} }}, + &http.Request{Header: http.Header{"Authorization": {"Basic " + "dXNlcm5hbWU6cGFzc3dvcmQ="}}}, session, json.RawMessage(`{"credentials":"bc842c31a9e54efe320d30d948be61291f3ceee4766e36ab25fa65243cd76e0e"}`), nil) - require.NoError(t, err) + require.NoError(t, err) }) }