From b48aaeaca8ef86f807ae187787e22db06441454d Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Sat, 19 Aug 2017 17:38:33 +1200 Subject: [PATCH 1/5] Add basic auth middleware --- auth/basic/README.md | 16 ++++++ auth/basic/middleware.go | 92 +++++++++++++++++++++++++++++++++++ auth/basic/middleware_test.go | 50 +++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 auth/basic/README.md create mode 100644 auth/basic/middleware.go create mode 100644 auth/basic/middleware_test.go diff --git a/auth/basic/README.md b/auth/basic/README.md new file mode 100644 index 000000000..9e77c0f7c --- /dev/null +++ b/auth/basic/README.md @@ -0,0 +1,16 @@ +`package auth/basic` provides a basic auth middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) + +## Usage + +```go +import httptransport "github.com/go-kit/kit/transport/http" + +httptransport.NewServer( + endpoint.Chain(AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm"))(makeUppercaseEndpoint()), + decodeMappingsRequest, + httptransport.EncodeJSONResponse, + httptransport.ServerBefore(httptransport.PopulateRequestContext), + ) +``` + +For AuthMiddleware to be able to pick up Authentication header from a http request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)``` \ No newline at end of file diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go new file mode 100644 index 000000000..1f603ded8 --- /dev/null +++ b/auth/basic/middleware.go @@ -0,0 +1,92 @@ +package basic + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/go-kit/kit/endpoint" + httptransport "github.com/go-kit/kit/transport/http" +) + +// AuthError represents generic Authorization error +type AuthError struct { + Realm string +} + +// StatusCode is an implemntation of StatusCoder interface in go-kit/http +func (AuthError) StatusCode() int { + return http.StatusUnauthorized +} + +// Error is an implemntation of Error interface +func (AuthError) Error() string { + return http.StatusText(http.StatusUnauthorized) +} + +// Headers is an implemntation of Headerer interface in go-kit/http +func (e AuthError) Headers() http.Header { + return http.Header{ + "Content-Type": []string{"text/plain; charset=utf-8"}, + "X-Content-Type-Options": []string{"nosniff"}, + "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}} +} + +func credsAreValid(givenUser, givenPass, requiredUser, requiredPass string) bool { + // Equalize lengths of supplied and required credentials + // by hashing them + givenUserBytes := sha256.Sum256([]byte(givenUser)) + givenPassBytes := sha256.Sum256([]byte(givenPass)) + requiredUserBytes := sha256.Sum256([]byte(requiredUser)) + requiredPassBytes := sha256.Sum256([]byte(requiredPass)) + + // Compare the supplied credentials to those set in our options + if subtle.ConstantTimeCompare(givenUserBytes[:], requiredUserBytes[:]) == 1 && + subtle.ConstantTimeCompare(givenPassBytes[:], requiredPassBytes[:]) == 1 { + return true + } + + return false +} + +// parseBasicAuth parses an HTTP Basic Authentication string. +// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). +func parseBasicAuth(auth string) (username, password string, ok bool) { + const prefix = "Basic " + if !strings.HasPrefix(auth, prefix) { + return + } + c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) + if err != nil { + return + } + cs := string(c) + s := strings.IndexByte(cs, ':') + if s < 0 { + return + } + return cs[:s], cs[s+1:], true +} + +// AuthMiddleware returns a Basic Authentication middleware for a particular user and password +func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) + givenUser, givenPass, ok := parseBasicAuth(auth) + if !ok { + return nil, AuthError{realm} + } + + if !credsAreValid(givenUser, givenPass, requiredUser, requiredPassword) { + return nil, AuthError{realm} + } + + return next(ctx, request) + } + } +} diff --git a/auth/basic/middleware_test.go b/auth/basic/middleware_test.go new file mode 100644 index 000000000..a34c6cf04 --- /dev/null +++ b/auth/basic/middleware_test.go @@ -0,0 +1,50 @@ +package basic + +import ( + "context" + "encoding/base64" + "fmt" + "testing" + + httptransport "github.com/go-kit/kit/transport/http" +) + +func TestWithBasicAuth(t *testing.T) { + requiredUser := "test-user" + requiredPassword := "test-pass" + realm := "test realm" + + type want struct { + result interface{} + err error + } + tests := []struct { + name string + authHeader string + want want + }{ + {"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, + {"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, + {"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}}, + {"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader) + + result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil) + if result != tt.want.result || err != tt.want.err { + t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err) + } + }) + } +} + +func makeAuthString(user string, password string) string { + data := []byte(fmt.Sprintf("%s:%s", user, password)) + return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data)) +} + +func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) { + return true, nil +} From 8c6ad4ea162f0a349a15ab4c6bec58f3cbe6b5c1 Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Mon, 28 Aug 2017 15:26:02 +1200 Subject: [PATCH 2/5] Basic Auth: optimize memory allocation. --- auth/basic/README.md | 4 ++-- auth/basic/middleware.go | 49 +++++++++++++++++----------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/auth/basic/README.md b/auth/basic/README.md index 9e77c0f7c..fe3a9de02 100644 --- a/auth/basic/README.md +++ b/auth/basic/README.md @@ -1,4 +1,4 @@ -`package auth/basic` provides a basic auth middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) +`package auth/basic` provides a Basic Authentication middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). ## Usage @@ -13,4 +13,4 @@ httptransport.NewServer( ) ``` -For AuthMiddleware to be able to pick up Authentication header from a http request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)``` \ No newline at end of file +For AuthMiddleware to be able to pick up the Authentication header from a HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```. \ No newline at end of file diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go index 1f603ded8..17b0b6156 100644 --- a/auth/basic/middleware.go +++ b/auth/basic/middleware.go @@ -1,6 +1,7 @@ package basic import ( + "bytes" "context" "crypto/sha256" "crypto/subtle" @@ -13,22 +14,22 @@ import ( httptransport "github.com/go-kit/kit/transport/http" ) -// AuthError represents generic Authorization error +// AuthError represents an authoriation error. type AuthError struct { Realm string } -// StatusCode is an implemntation of StatusCoder interface in go-kit/http +// StatusCode is an iimplementation of the StatusCoder interface in go-kit/http. func (AuthError) StatusCode() int { return http.StatusUnauthorized } -// Error is an implemntation of Error interface +// Error is an implementation of the Error interface. func (AuthError) Error() string { return http.StatusText(http.StatusUnauthorized) } -// Headers is an implemntation of Headerer interface in go-kit/http +// Headers is an implemntation of the Headerer interface in go-kit/http. func (e AuthError) Headers() http.Header { return http.Header{ "Content-Type": []string{"text/plain; charset=utf-8"}, @@ -36,26 +37,9 @@ func (e AuthError) Headers() http.Header { "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}} } -func credsAreValid(givenUser, givenPass, requiredUser, requiredPass string) bool { - // Equalize lengths of supplied and required credentials - // by hashing them - givenUserBytes := sha256.Sum256([]byte(givenUser)) - givenPassBytes := sha256.Sum256([]byte(givenPass)) - requiredUserBytes := sha256.Sum256([]byte(requiredUser)) - requiredPassBytes := sha256.Sum256([]byte(requiredPass)) - - // Compare the supplied credentials to those set in our options - if subtle.ConstantTimeCompare(givenUserBytes[:], requiredUserBytes[:]) == 1 && - subtle.ConstantTimeCompare(givenPassBytes[:], requiredPassBytes[:]) == 1 { - return true - } - - return false -} - // parseBasicAuth parses an HTTP Basic Authentication string. -// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). -func parseBasicAuth(auth string) (username, password string, ok bool) { +// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). +func parseBasicAuth(auth string) (username, password []byte, ok bool) { const prefix = "Basic " if !strings.HasPrefix(auth, prefix) { return @@ -64,16 +48,19 @@ func parseBasicAuth(auth string) (username, password string, ok bool) { if err != nil { return } - cs := string(c) - s := strings.IndexByte(cs, ':') + + s := bytes.IndexByte(c, ':') if s < 0 { return } - return cs[:s], cs[s+1:], true + return c[:s], c[s+1:], true } -// AuthMiddleware returns a Basic Authentication middleware for a particular user and password +// AuthMiddleware returns a Basic Authentication middleware for a particular user and password. func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { + requiredUserBytes := sha256.Sum256([]byte(requiredUser)) + requiredPassBytes := sha256.Sum256([]byte(requiredPassword)) + return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) @@ -82,7 +69,13 @@ func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middl return nil, AuthError{realm} } - if !credsAreValid(givenUser, givenPass, requiredUser, requiredPassword) { + // Equalize lengths of supplied and required credentials by hashing them. + givenUserBytes := sha256.Sum256(givenUser) + givenPassBytes := sha256.Sum256(givenPass) + + // Compare the supplied credentials to those set in our options. + if subtle.ConstantTimeCompare(givenUserBytes[:], requiredUserBytes[:]) == 0 || + subtle.ConstantTimeCompare(givenPassBytes[:], requiredPassBytes[:]) == 0 { return nil, AuthError{realm} } From 37a20808055183abe2171946ebdf1dee7e1b887e Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Tue, 29 Aug 2017 22:01:23 +1200 Subject: [PATCH 3/5] cache required creds' slices --- auth/basic/README.md | 2 +- auth/basic/middleware.go | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/auth/basic/README.md b/auth/basic/README.md index fe3a9de02..b681ac7f2 100644 --- a/auth/basic/README.md +++ b/auth/basic/README.md @@ -13,4 +13,4 @@ httptransport.NewServer( ) ``` -For AuthMiddleware to be able to pick up the Authentication header from a HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```. \ No newline at end of file +For AuthMiddleware to be able to pick up the Authentication header from an HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```. \ No newline at end of file diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go index 17b0b6156..ffb224253 100644 --- a/auth/basic/middleware.go +++ b/auth/basic/middleware.go @@ -14,12 +14,12 @@ import ( httptransport "github.com/go-kit/kit/transport/http" ) -// AuthError represents an authoriation error. +// AuthError represents an authorization error. type AuthError struct { Realm string } -// StatusCode is an iimplementation of the StatusCoder interface in go-kit/http. +// StatusCode is an implementation of the StatusCoder interface in go-kit/http. func (AuthError) StatusCode() int { return http.StatusUnauthorized } @@ -29,7 +29,7 @@ func (AuthError) Error() string { return http.StatusText(http.StatusUnauthorized) } -// Headers is an implemntation of the Headerer interface in go-kit/http. +// Headers is an implementation of the Headerer interface in go-kit/http. func (e AuthError) Headers() http.Header { return http.Header{ "Content-Type": []string{"text/plain; charset=utf-8"}, @@ -56,26 +56,31 @@ func parseBasicAuth(auth string) (username, password []byte, ok bool) { return c[:s], c[s+1:], true } +// Returns a hash of a given slice. +func toHashSlice(s []byte) []byte { + hash := sha256.Sum256(s) + return hash[:] +} + // AuthMiddleware returns a Basic Authentication middleware for a particular user and password. func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { - requiredUserBytes := sha256.Sum256([]byte(requiredUser)) - requiredPassBytes := sha256.Sum256([]byte(requiredPassword)) + requiredUserBytes := toHashSlice([]byte(requiredUser)) + requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) - givenUser, givenPass, ok := parseBasicAuth(auth) + givenUser, givenPassword, ok := parseBasicAuth(auth) if !ok { return nil, AuthError{realm} } // Equalize lengths of supplied and required credentials by hashing them. - givenUserBytes := sha256.Sum256(givenUser) - givenPassBytes := sha256.Sum256(givenPass) + givenUserBytes := toHashSlice(givenUser) + givenPasswordBytes := toHashSlice(givenPassword) - // Compare the supplied credentials to those set in our options. - if subtle.ConstantTimeCompare(givenUserBytes[:], requiredUserBytes[:]) == 0 || - subtle.ConstantTimeCompare(givenPassBytes[:], requiredPassBytes[:]) == 0 { + if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || + subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { return nil, AuthError{realm} } From 6b4ca49912a198b104a8cc31ab2db48bd22dc10d Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Wed, 30 Aug 2017 10:56:25 +1200 Subject: [PATCH 4/5] Clean up comment --- auth/basic/middleware.go | 1 - 1 file changed, 1 deletion(-) diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go index ffb224253..eeb231712 100644 --- a/auth/basic/middleware.go +++ b/auth/basic/middleware.go @@ -75,7 +75,6 @@ func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middl return nil, AuthError{realm} } - // Equalize lengths of supplied and required credentials by hashing them. givenUserBytes := toHashSlice(givenUser) givenPasswordBytes := toHashSlice(givenPassword) From 355d3e3b367f8f2e295be2dcf96bc0bb84cd468d Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Sat, 2 Sep 2017 21:57:51 +1200 Subject: [PATCH 5/5] improve error handling and style --- auth/basic/README.md | 8 ++++++-- auth/basic/middleware.go | 9 +++++++-- auth/basic/middleware_test.go | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/auth/basic/README.md b/auth/basic/README.md index b681ac7f2..26d6c4b31 100644 --- a/auth/basic/README.md +++ b/auth/basic/README.md @@ -1,4 +1,8 @@ -`package auth/basic` provides a Basic Authentication middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). +This package provides a Basic Authentication middleware. + +It'll try to compare credentials from Authentication request header to a username/password pair in middleware constructor. + +More details about this type of authentication can be found in [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). ## Usage @@ -6,7 +10,7 @@ import httptransport "github.com/go-kit/kit/transport/http" httptransport.NewServer( - endpoint.Chain(AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm"))(makeUppercaseEndpoint()), + AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm")(makeUppercaseEndpoint()), decodeMappingsRequest, httptransport.EncodeJSONResponse, httptransport.ServerBefore(httptransport.PopulateRequestContext), diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go index eeb231712..ad7e4085d 100644 --- a/auth/basic/middleware.go +++ b/auth/basic/middleware.go @@ -34,7 +34,8 @@ func (e AuthError) Headers() http.Header { return http.Header{ "Content-Type": []string{"text/plain; charset=utf-8"}, "X-Content-Type-Options": []string{"nosniff"}, - "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}} + "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}, + } } // parseBasicAuth parses an HTTP Basic Authentication string. @@ -69,7 +70,11 @@ func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middl return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) + auth, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) + if !ok { + return nil, AuthError{realm} + } + givenUser, givenPassword, ok := parseBasicAuth(auth) if !ok { return nil, AuthError{realm} diff --git a/auth/basic/middleware_test.go b/auth/basic/middleware_test.go index a34c6cf04..9ad330ebb 100644 --- a/auth/basic/middleware_test.go +++ b/auth/basic/middleware_test.go @@ -20,9 +20,11 @@ func TestWithBasicAuth(t *testing.T) { } tests := []struct { name string - authHeader string + authHeader interface{} want want }{ + {"Isn't valid with nil header", nil, want{nil, AuthError{realm}}}, + {"Isn't valid with non-string header", 42, want{nil, AuthError{realm}}}, {"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, {"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, {"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}},