refactor: split up auth middlewares

This commit is contained in:
franklin 2021-09-06 22:11:04 +02:00
parent ef9575a329
commit a66a86aa58
4 changed files with 146 additions and 88 deletions

View File

@ -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)
}

View File

@ -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)
})
}
}

View File

@ -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)
})
}
}

View File

@ -0,0 +1,9 @@
package httputils
import (
"golang.org/x/crypto/bcrypt"
)
func HashBasicAuthPassword(v []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(v, bcrypt.DefaultCost)
}