From 0ecd8528fcd0d01e5d7d85bf5216c48cad899dc9 Mon Sep 17 00:00:00 2001 From: johnabass Date: Mon, 19 Aug 2024 08:07:49 -0700 Subject: [PATCH 1/2] added BasicAuth function to encoding basic auth strings --- basculehttp/basic.go | 11 ++++++++ basculehttp/basic_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 basculehttp/basic_test.go diff --git a/basculehttp/basic.go b/basculehttp/basic.go index 5dbfcd7..08289f2 100644 --- a/basculehttp/basic.go +++ b/basculehttp/basic.go @@ -4,6 +4,7 @@ package basculehttp import ( + "bytes" "context" "encoding/base64" "strings" @@ -62,3 +63,13 @@ func (BasicTokenParser) Parse(_ context.Context, value string) (bascule.Token, e return nil, bascule.ErrInvalidCredentials } + +// BasicAuth produces the basic authorization string described by RFC 2617. +func BasicAuth(userName, password string) string { + var b bytes.Buffer + b.WriteString(userName) + b.WriteRune(':') + b.WriteString(password) + + return base64.StdEncoding.EncodeToString(b.Bytes()) +} diff --git a/basculehttp/basic_test.go b/basculehttp/basic_test.go new file mode 100644 index 0000000..9d2a2fc --- /dev/null +++ b/basculehttp/basic_test.go @@ -0,0 +1,59 @@ +package basculehttp + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/bascule/v1" +) + +type BasicTestSuite struct { + suite.Suite +} + +func (suite *BasicTestSuite) TestBasicAuth() { + encoded := BasicAuth("Aladdin", "open sesame") + + // ripped right from RFC 2617 ... + suite.Equal("QWxhZGRpbjpvcGVuIHNlc2FtZQ==", encoded) +} + +func (suite *BasicTestSuite) TestBasicTokenParser() { + suite.Run("Invalid", func() { + p := BasicTokenParser{} + token, err := p.Parse(context.Background(), "^%$@!()$kldfj34729$(&fhd") // hopelessly invalid ... + + suite.ErrorIs(err, bascule.ErrInvalidCredentials) + suite.Nil(token) + }) + + suite.Run("ImproperlyFormatted", func() { + p := BasicTokenParser{} + token, err := p.Parse( + context.Background(), + base64.StdEncoding.EncodeToString([]byte("missing colon")), + ) + + suite.ErrorIs(err, bascule.ErrInvalidCredentials) + suite.Nil(token) + }) + + suite.Run("Success", func() { + p := BasicTokenParser{} + token, err := p.Parse(context.Background(), "QWxhZGRpbjpvcGVuIHNlc2FtZQ==") + + suite.NoError(err) + suite.Require().NotNil(token) + suite.Equal("Aladdin", token.Principal()) + + suite.Require().Implements((*BasicToken)(nil), token) + suite.Equal("Aladdin", token.(BasicToken).UserName()) + suite.Equal("open sesame", token.(BasicToken).Password()) + }) +} + +func TestBasic(t *testing.T) { + suite.Run(t, new(BasicTestSuite)) +} From 16a52f35dfafbf6e6d987bab4ae9aace5e7782e5 Mon Sep 17 00:00:00 2001 From: johnabass Date: Mon, 19 Aug 2024 08:33:09 -0700 Subject: [PATCH 2/2] chore: fleshed out all the unit tests surrounding authorization header parsing --- basculehttp/authorization.go | 14 ++-- basculehttp/authorization_test.go | 124 ++++++++++++++++++++++++++++++ basculehttp/basic_test.go | 3 + basculehttp/middleware_test.go | 58 +------------- basculehttp/scheme_test.go | 28 +++++++ basculehttp/testSuite_test.go | 64 +++++++++++++++ 6 files changed, 231 insertions(+), 60 deletions(-) create mode 100644 basculehttp/authorization_test.go create mode 100644 basculehttp/scheme_test.go create mode 100644 basculehttp/testSuite_test.go diff --git a/basculehttp/authorization.go b/basculehttp/authorization.go index 2ab4805..34535f3 100644 --- a/basculehttp/authorization.go +++ b/basculehttp/authorization.go @@ -21,11 +21,7 @@ const ( var ( // ErrInvalidAuthorization indicates an authorization header value did not // correspond to the standard. - ErrInvalidAuthorization = errors.New("invalidation authorization") - - // ErrMissingAuthorization indicates that no authorization header was - // present in the source HTTP request. - ErrMissingAuthorization = errors.New("missing authorization") + ErrInvalidAuthorization = errors.New("invalid authorization") ) // ParseAuthorization parses an authorization value typically passed in @@ -48,6 +44,7 @@ func ParseAuthorization(raw string) (s Scheme, v string, err error) { return } +// AuthorizationParserOption is a configurable option for an AuthorizationParser. type AuthorizationParserOption interface { apply(*AuthorizationParser) error } @@ -83,6 +80,9 @@ func WithBasic() AuthorizationParserOption { } // AuthorizationParsers is a bascule.TokenParser that handles the Authorization header. +// +// By default, this parser will use the standard Authorization header, which can be +// changed via with WithAuthorizationHeader option. type AuthorizationParser struct { header string parsers map[Scheme]bascule.TokenParser[string] @@ -111,6 +111,8 @@ func NewAuthorizationParser(opts ...AuthorizationParserOption) (*AuthorizationPa // Parse extracts the appropriate header, Authorization by default, and parses the // scheme and value. Schemes are case-insensitive, e.g. BASIC and Basic are the same scheme. // +// If no authorization header is found in the request, this method returns ErrMissingCredentials. +// // If a token parser is registered for the given scheme, that token parser is invoked. // Otherwise, UnsupportedSchemeError is returned, indicating the scheme in question. func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request) (bascule.Token, error) { @@ -121,7 +123,7 @@ func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request) scheme, value, err := ParseAuthorization(authValue) if err != nil { - return nil, err + return nil, bascule.ErrInvalidCredentials } p, registered := ap.parsers[scheme.lower()] diff --git a/basculehttp/authorization_test.go b/basculehttp/authorization_test.go new file mode 100644 index 0000000..ae7f4e1 --- /dev/null +++ b/basculehttp/authorization_test.go @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/bascule/v1" +) + +// withAuthorizationParserOptionErr is an option that returns an error. +// These tests need this since no options currently return errors +// for an AuthorizationParser. +func withAuthorizationParserOptionErr(err error) AuthorizationParserOption { + return authorizationParserOptionFunc(func(*AuthorizationParser) error { + return err + }) +} + +type AuthorizationTestSuite struct { + TestSuite +} + +// newAuthorizationParser produces a parser from a set of options that must assert as valid. +func (suite *AuthorizationTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser { + ap, err := NewAuthorizationParser(opts...) + suite.Require().NoError(err) + suite.Require().NotNil(ap) + return ap +} + +func (suite *AuthorizationTestSuite) TestBasicAuthSuccess() { + suite.Run("DefaultHeader", func() { + var ( + ap = suite.newAuthorizationParser( + WithBasic(), + ) + + request = suite.newBasicAuthRequest() + ) + + token, err := ap.Parse(context.Background(), request) + suite.NoError(err) + suite.assertBasicToken(token) + }) + + suite.Run("Custom", func() { + var ( + ap = suite.newAuthorizationParser( + WithAuthorizationHeader("Auth-Custom"), + WithScheme(Scheme("Custom"), BasicTokenParser{}), + ) + + request = suite.newRequest() + ) + + request.Header.Set("Auth-Custom", "Custom "+suite.basicAuth()) + token, err := ap.Parse(context.Background(), request) + suite.NoError(err) + suite.assertBasicToken(token) + }) +} + +func (suite *AuthorizationTestSuite) TestMissingCredentials() { + var ( + ap = suite.newAuthorizationParser( + WithBasic(), + ) + + request = suite.newRequest() + ) + + token, err := ap.Parse(context.Background(), request) + suite.ErrorIs(err, bascule.ErrMissingCredentials) + suite.Nil(token) +} + +func (suite *AuthorizationTestSuite) TestInvalidCredentials() { + var ( + ap = suite.newAuthorizationParser( + WithBasic(), + ) + + request = suite.newRequest() + ) + + request.Header.Set(DefaultAuthorizationHeader, "\t") + token, err := ap.Parse(context.Background(), request) + suite.ErrorIs(err, bascule.ErrInvalidCredentials) + suite.Nil(token) +} + +func (suite *AuthorizationTestSuite) TestUnsupportedScheme() { + var ( + ap = suite.newAuthorizationParser( + WithBasic(), + ) + + request = suite.newRequest() + ) + + request.Header.Set(DefaultAuthorizationHeader, "Unsupported xyz") + token, err := ap.Parse(context.Background(), request) + suite.Nil(token) + + var use *UnsupportedSchemeError + suite.Require().ErrorAs(err, &use) + suite.Equal(Scheme("Unsupported"), use.Scheme) +} + +func (suite *AuthorizationTestSuite) TestOptionError() { + expectedErr := errors.New("expected") + ap, err := NewAuthorizationParser(withAuthorizationParserOptionErr(expectedErr)) + suite.ErrorIs(err, expectedErr) + suite.Nil(ap) +} + +func TestAuthorization(t *testing.T) { + suite.Run(t, new(AuthorizationTestSuite)) +} diff --git a/basculehttp/basic_test.go b/basculehttp/basic_test.go index 9d2a2fc..89824c2 100644 --- a/basculehttp/basic_test.go +++ b/basculehttp/basic_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + package basculehttp import ( diff --git a/basculehttp/middleware_test.go b/basculehttp/middleware_test.go index 486b103..d995a54 100644 --- a/basculehttp/middleware_test.go +++ b/basculehttp/middleware_test.go @@ -16,57 +16,7 @@ import ( ) type MiddlewareTestSuite struct { - suite.Suite - - expectedPrincipal string - expectedPassword string - expectedToken bascule.Token -} - -func (suite *MiddlewareTestSuite) SetupSuite() { - suite.expectedPrincipal = "testPrincipal" - suite.expectedPassword = "test_password" - suite.expectedToken = basicToken{ - userName: suite.expectedPrincipal, - password: suite.expectedPassword, - } -} - -// newRequest creates a standardized test request, devoid of any authorization. -func (suite *MiddlewareTestSuite) newRequest() *http.Request { - return httptest.NewRequest("GET", "/test", nil) -} - -// newBasicAuthRequest creates a new test request configured with valid basic auth. -func (suite *MiddlewareTestSuite) newBasicAuthRequest() *http.Request { - request := suite.newRequest() - request.SetBasicAuth(suite.expectedPrincipal, suite.expectedPassword) - return request -} - -// assertBasicAuthRequest asserts that the given request matches this suite's expectations. -func (suite *MiddlewareTestSuite) assertBasicAuthRequest(request *http.Request) { - suite.Require().NotNil(request) - suite.Equal("GET", request.Method) - suite.Equal("/test", request.URL.String()) -} - -// assertBasicAuthToken asserts that the token matches this suite's expectations. -func (suite *MiddlewareTestSuite) assertBasicAuthToken(token bascule.Token) { - suite.Require().NotNil(token) - suite.Equal(suite.expectedPrincipal, token.Principal()) - suite.Require().Implements((*BasicToken)(nil), token) - suite.Equal(suite.expectedPrincipal, token.(BasicToken).UserName()) - suite.Equal(suite.expectedPassword, token.(BasicToken).Password()) -} - -// newAuthorizationParser creates an AuthorizationParser that is expected to be valid. -// Assertions as to validity are made prior to returning. -func (suite *MiddlewareTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser { - ap, err := NewAuthorizationParser(opts...) - suite.Require().NoError(err) - suite.Require().NotNil(ap) - return ap + TestSuite } // newAuthenticator creates a bascule.Authenticator that is expected to be valid. @@ -325,14 +275,14 @@ func (suite *MiddlewareTestSuite) testBasicAuthSuccess() { bascule.WithApproverFuncs( func(_ context.Context, request *http.Request, token bascule.Token) error { suite.assertBasicAuthRequest(request) - suite.assertBasicAuthToken(token) + suite.assertBasicToken(token) return nil }, ), bascule.WithAuthorizeListenerFuncs( func(e bascule.AuthorizeEvent[*http.Request]) { suite.assertBasicAuthRequest(e.Resource) - suite.assertBasicAuthToken(e.Token) + suite.assertBasicToken(e.Token) authorizeEvent = true }, ), @@ -434,7 +384,7 @@ func (suite *MiddlewareTestSuite) testBasicAuthAuthorizerError() { bascule.WithApproverFuncs( func(_ context.Context, resource *http.Request, token bascule.Token) error { suite.assertBasicAuthRequest(resource) - suite.assertBasicAuthToken(token) + suite.assertBasicToken(token) return expectedErr }, ), diff --git a/basculehttp/scheme_test.go b/basculehttp/scheme_test.go new file mode 100644 index 0000000..22425f3 --- /dev/null +++ b/basculehttp/scheme_test.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/suite" +) + +type SchemeTestSuite struct { + suite.Suite +} + +func (suite *SchemeTestSuite) TestUnsupportedSchemeError() { + use := &UnsupportedSchemeError{ + Scheme: Scheme("Unsupported"), + } + + suite.Equal(http.StatusUnauthorized, use.StatusCode()) + suite.Contains(use.Error(), "Unsupported") +} + +func TestScheme(t *testing.T) { + suite.Run(t, new(SchemeTestSuite)) +} diff --git a/basculehttp/testSuite_test.go b/basculehttp/testSuite_test.go new file mode 100644 index 0000000..0213d44 --- /dev/null +++ b/basculehttp/testSuite_test.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "net/http" + "net/http/httptest" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/bascule/v1" +) + +const ( + expectedPrincipal = "testPrincipal" + expectedPassword = "test_password" +) + +// TestSuite is a common suite that exposes some useful behaviors. +type TestSuite struct { + suite.Suite +} + +// newRequest creates a standardized test request, devoid of any authorization. +func (suite *TestSuite) newRequest() *http.Request { + return httptest.NewRequest("GET", "/test", nil) +} + +// assertRequest asserts that the given request matches the one created by newRequest. +func (suite *TestSuite) assertBasicAuthRequest(request *http.Request) { + suite.Require().NotNil(request) + suite.Equal("GET", request.Method) + suite.Equal("/test", request.URL.String()) +} + +// newBasicAuthRequest creates a new test request configured with valid basic auth. +func (suite *TestSuite) newBasicAuthRequest() *http.Request { + request := suite.newRequest() + request.SetBasicAuth(expectedPrincipal, expectedPassword) + return request +} + +// basicAuth produces a formatted basic authorization string using this suite's expectations. +func (suite *TestSuite) basicAuth() string { + return BasicAuth(expectedPrincipal, expectedPassword) +} + +// assertBasicToken asserts that the token matches the one created by newBasicToken. +func (suite *TestSuite) assertBasicToken(token bascule.Token) { + suite.Require().NotNil(token) + suite.Equal(expectedPrincipal, token.Principal()) + suite.Require().Implements((*BasicToken)(nil), token) + suite.Equal(expectedPrincipal, token.(BasicToken).UserName()) + suite.Equal(expectedPassword, token.(BasicToken).Password()) +} + +// newAuthorizationParser creates an AuthorizationParser that is expected to be valid. +// Assertions as to validity are made prior to returning. +func (suite *TestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser { + ap, err := NewAuthorizationParser(opts...) + suite.Require().NoError(err) + suite.Require().NotNil(ap) + return ap +}