mirror of
https://github.com/foomo/keel.git
synced 2025-10-16 12:35:34 +00:00
refactor: split up auth middlewares
This commit is contained in:
parent
ef9575a329
commit
a66a86aa58
@ -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)
|
|
||||||
}
|
|
||||||
74
net/http/middleware/basicauth.go
Normal file
74
net/http/middleware/basicauth.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
63
net/http/middleware/tokenauth.go
Normal file
63
net/http/middleware/tokenauth.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
9
utils/net/http/basicauth.go
Normal file
9
utils/net/http/basicauth.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user