From a66a86aa584ae4c9a5fdbb076e52104a5b51536a Mon Sep 17 00:00:00 2001 From: franklin Date: Mon, 6 Sep 2021 22:11:04 +0200 Subject: [PATCH] refactor: split up auth middlewares --- net/http/middleware/auth.go | 88 -------------------------------- net/http/middleware/basicauth.go | 74 +++++++++++++++++++++++++++ net/http/middleware/tokenauth.go | 63 +++++++++++++++++++++++ utils/net/http/basicauth.go | 9 ++++ 4 files changed, 146 insertions(+), 88 deletions(-) delete mode 100644 net/http/middleware/auth.go create mode 100644 net/http/middleware/basicauth.go create mode 100644 net/http/middleware/tokenauth.go create mode 100644 utils/net/http/basicauth.go diff --git a/net/http/middleware/auth.go b/net/http/middleware/auth.go deleted file mode 100644 index 21df2f9..0000000 --- a/net/http/middleware/auth.go +++ /dev/null @@ -1,88 +0,0 @@ -package middleware - -import ( - "crypto/subtle" - "net/http" - "strings" - - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" - - "github.com/foomo/keel/log" -) - -func BearerAuth(bearerToken string) Middleware { - bearerPrefix := "Bearer " - return func(l *zap.Logger, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(bearerToken, bearerPrefix) { - w.WriteHeader(http.StatusUnauthorized) - if _, err := w.Write([]byte("malformed token")); err != nil { - log.WithError(l, err).Error("failed to write http response") - } - return - } - - authHeader := strings.Replace(bearerToken, bearerPrefix, "", 1) - if subtle.ConstantTimeCompare([]byte(authHeader), []byte(bearerToken)) == 1 { - next.ServeHTTP(w, r) - return - } - - w.WriteHeader(http.StatusUnauthorized) - if _, err := w.Write([]byte("Unauthorized")); err != nil { - log.WithError(l, err).Error("failed to write http response") - } - }) - } -} - -// BasicAuth hashes the password when called and returns a middleware. -// NOTE: The error handling only takes place on incomming http requests. -// Therefore (and because of security) it is advised to hash the password -// beforehand and use BasicAuthBcryptHash. -func BasicAuth(user, password string) Middleware { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return func(l *zap.Logger, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - l.Error("unable to create password hash", zap.Error(err)) - w.WriteHeader(http.StatusInternalServerError) - }) - } - } - - return BasicAuthBcryptHash(user, string(hashedPassword)) -} - -// BasicAuthBcryptHash uses a plain text user name an a bcrypt salted hash of -// the password in order to authenticate the incomming http request. -func BasicAuthBcryptHash(user, hashedPassword string) Middleware { - return func(l *zap.Logger, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // basic auth from request header - u, p, ok := r.BasicAuth() - if !ok || len(strings.TrimSpace(u)) < 1 || len(strings.TrimSpace(p)) < 1 { - unauthorised(w) - return - } - - // Compare the username and password hash with the ones in the request - userMatch := (subtle.ConstantTimeCompare([]byte(u), []byte(user)) == 1) - errP := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(p)) - if !userMatch || errP != nil { - unauthorised(w) - return - } - - // If required, Context could be updated to include authentication - // related data so that it could be used in consequent steps. - next.ServeHTTP(w, r) - }) - } -} - -func unauthorised(w http.ResponseWriter) { - w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") - w.WriteHeader(http.StatusUnauthorized) -} diff --git a/net/http/middleware/basicauth.go b/net/http/middleware/basicauth.go new file mode 100644 index 0000000..81ac51b --- /dev/null +++ b/net/http/middleware/basicauth.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "crypto/subtle" + "fmt" + "net/http" + "strings" + + "github.com/pkg/errors" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + + httputils "github.com/foomo/keel/utils/net/http" +) + +type ( + BasicAuthOptions struct { + Realm string + } + BasicAuthOption func(*BasicAuthOptions) +) + +// GetDefaultBasicAuthOptions returns the default options +func GetDefaultBasicAuthOptions() BasicAuthOptions { + return BasicAuthOptions{ + Realm: "Restricted", + } +} + +// BasicAuthWithRealm middleware option +func BasicAuthWithRealm(v string) BasicAuthOption { + return func(o *BasicAuthOptions) { + o.Realm = v + } +} + +// BasicAuth middleware +func BasicAuth(username string, passwordHash []byte, opts ...BasicAuthOption) Middleware { + options := GetDefaultBasicAuthOptions() + for _, opt := range opts { + if opt != nil { + opt(&options) + } + } + return BasicAuthWithOptions(username, passwordHash, options) +} + +// BasicAuthWithOptions middleware +func BasicAuthWithOptions(username string, passwordHash []byte, opts BasicAuthOptions) Middleware { + return func(l *zap.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // basic auth from request header + u, p, ok := r.BasicAuth() + if !ok || len(strings.TrimSpace(u)) < 1 || len(strings.TrimSpace(p)) < 1 { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", opts.Realm)) + httputils.UnauthorizedServerError(l, w, r, errors.New("missing basic auth credentials")) + return + } + + // Compare the username and password hash with the ones in the request + userMatch := subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1 + errP := bcrypt.CompareHashAndPassword(passwordHash, []byte(p)) + if !userMatch || errP != nil { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", opts.Realm)) + httputils.UnauthorizedServerError(l, w, r, errors.New("invalid basic auth credentials")) + return + } + + // If required, Context could be updated to include authentication + // related data so that it could be used in consequent steps. + next.ServeHTTP(w, r) + }) + } +} diff --git a/net/http/middleware/tokenauth.go b/net/http/middleware/tokenauth.go new file mode 100644 index 0000000..e6b9e9f --- /dev/null +++ b/net/http/middleware/tokenauth.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" + + "github.com/pkg/errors" + "go.uber.org/zap" + + httputils "github.com/foomo/keel/utils/net/http" +) + +type ( + TokenAuthOptions struct { + // TokenProvider function to retrieve the token + TokenProvider TokenProvider + } + TokenAuthOption func(*TokenAuthOptions) +) + +// GetDefaultTokenAuthOptions returns the default options +func GetDefaultTokenAuthOptions() TokenAuthOptions { + return TokenAuthOptions{ + TokenProvider: HeaderTokenProvider(), + } +} + +// TokenAuthWithTokenProvider middleware option +func TokenAuthWithTokenProvider(v TokenProvider) TokenAuthOption { + return func(o *TokenAuthOptions) { + o.TokenProvider = v + } +} + +// TokenAuth middleware +func TokenAuth(token string, opts ...TokenAuthOption) Middleware { + options := GetDefaultTokenAuthOptions() + for _, opt := range opts { + if opt != nil { + opt(&options) + } + } + return TokenAuthWithOptions(token, options) +} + +// TokenAuthWithOptions middleware +func TokenAuthWithOptions(token string, opts TokenAuthOptions) Middleware { + return func(l *zap.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if value, err := opts.TokenProvider(r); err != nil { + httputils.UnauthorizedServerError(l, w, r, errors.Wrap(err, "failed to retrieve token")) + return + } else if value == "" { + httputils.UnauthorizedServerError(l, w, r, errors.New("missing token")) + return + } else if subtle.ConstantTimeCompare([]byte(value), []byte(token)) != 1 { + httputils.UnauthorizedServerError(l, w, r, errors.New("invalid token")) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/utils/net/http/basicauth.go b/utils/net/http/basicauth.go new file mode 100644 index 0000000..efe6c33 --- /dev/null +++ b/utils/net/http/basicauth.go @@ -0,0 +1,9 @@ +package httputils + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashBasicAuthPassword(v []byte) ([]byte, error) { + return bcrypt.GenerateFromPassword(v, bcrypt.DefaultCost) +}