bouncer/internal/proxy/director/layer/authn/basic/authenticator.go

76 lines
2.0 KiB
Go

package basic
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/bcrypt"
)
type Authenticator struct {
}
// Authenticate implements authn.Authenticator.
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
options, err := fromStoreOptions(layer.Options)
if err != nil {
return nil, errors.WithStack(err)
}
username, password, ok := r.BasicAuth()
unauthorized := func() {
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s", charset="UTF-8"`, stripNonASCII(options.Realm)))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
if !ok {
unauthorized()
return nil, errors.WithStack(authn.ErrSkipRequest)
}
for _, userInfo := range options.Users {
if matches := a.matchUser(userInfo, username, password); !matches {
continue
}
metricAuthorizedTotal.With(prometheus.Labels{
metricLabelLayer: string(layer.Name),
metricLabelProxy: string(layer.Proxy),
}).Add(1)
user := authn.NewUser(userInfo.Username, userInfo.Attributes)
return user, nil
}
metricForbiddenTotal.With(prometheus.Labels{
metricLabelLayer: string(layer.Name),
metricLabelProxy: string(layer.Proxy),
}).Add(1)
unauthorized()
return nil, errors.WithStack(authn.ErrSkipRequest)
}
func (a *Authenticator) matchUser(user User, username, password string) bool {
usernameHash := sha256.Sum256([]byte(username))
expectedUsernameHash := sha256.Sum256([]byte(user.Username))
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil
return usernameMatch && passwordMatch
}
var (
_ authn.Authenticator = &Authenticator{}
)