add minimum password length check

This commit is contained in:
Christoph Haas
2025-05-16 09:55:35 +02:00
parent 1394be2341
commit e9005b1b90
13 changed files with 129 additions and 13 deletions

View File

@@ -99,6 +99,8 @@ type Authenticator interface {
LoggedIn(scopes ...Scope) func(next http.Handler) http.Handler
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
UserIdMatch(idParameter string) func(next http.Handler) http.Handler
// InfoOnly only add user info to the request context. No login check is performed.
InfoOnly() func(next http.Handler) http.Handler
}
type Session interface {

View File

@@ -47,7 +47,7 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup := g.Mount("/config")
apiGroup.HandleFunc("GET /frontend.js", e.handleConfigJsGet())
apiGroup.HandleFunc("GET /settings", e.handleSettingsGet())
apiGroup.With(e.authenticator.InfoOnly()).HandleFunc("GET /settings", e.handleSettingsGet())
}
// handleConfigJsGet returns a gorm Handler function.
@@ -108,6 +108,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
SelfProvisioning: e.cfg.Core.SelfProvisioningAllowed,
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
})
}
}

View File

@@ -72,6 +72,32 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
}
}
// InfoOnly only checks if the user is logged in and adds the user id to the context.
// If the user is not logged in, the context user id is set to domain.CtxUnknownUserId.
func (h AuthenticationHandler) InfoOnly() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := h.session.GetData(r.Context())
var newContext context.Context
if !session.LoggedIn {
newContext = domain.SetUserInfo(r.Context(), domain.DefaultContextUserInfo())
} else {
newContext = domain.SetUserInfo(r.Context(), &domain.ContextUserInfo{
Id: domain.UserIdentifier(session.UserIdentifier),
IsAdmin: session.IsAdmin,
})
}
r = r.WithContext(newContext)
// Continue down the chain to Handler etc
next.ServeHTTP(w, r)
})
}
}
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
func (h AuthenticationHandler) UserIdMatch(idParameter string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {

View File

@@ -11,4 +11,5 @@ type Settings struct {
SelfProvisioning bool `json:"SelfProvisioning"`
ApiAdminOnly bool `json:"ApiAdminOnly"`
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
MinPasswordLength int `json:"MinPasswordLength"`
}

View File

@@ -364,6 +364,10 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
}
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
return errors.Join(fmt.Errorf("password too weak: %w", err), domain.ErrInvalidData)
}
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
}
@@ -418,7 +422,11 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
// database users must have a password
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
return fmt.Errorf("missing password: %w", domain.ErrInvalidData)
}
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
return errors.Join(fmt.Errorf("password too weak: %w", err), domain.ErrInvalidData)
}
return nil

View File

@@ -18,6 +18,9 @@ type Auth struct {
Ldap []LdapProvider `yaml:"ldap"`
// Webauthn contains the configuration for the WebAuthn authenticator.
WebAuthn WebauthnConfig `yaml:"webauthn"`
// MinPasswordLength is the minimum password length for user accounts. This also applies to the admin user.
// It is encouraged to set this value to at least 16 characters.
MinPasswordLength int `yaml:"min_password_length"`
}
// BaseFields contains the basic fields that are used to map user information from the authentication providers.

View File

@@ -101,7 +101,7 @@ func defaultConfig() *Config {
cfg := &Config{}
cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal"
cfg.Core.AdminPassword = "wgportal-default"
cfg.Core.AdminApiToken = "" // by default, the API access is disabled
cfg.Core.ImportExisting = true
cfg.Core.RestoreState = true
@@ -165,6 +165,7 @@ func defaultConfig() *Config {
cfg.Webhook.Timeout = 10 * time.Second
cfg.Auth.WebAuthn.Enabled = true
cfg.Auth.MinPasswordLength = 16
return cfg
}

View File

@@ -88,6 +88,22 @@ func (u *User) CanChangePassword() error {
return errors.New("password change only allowed for database source")
}
func (u *User) HasWeakPassword(minLength int) error {
if u.Source != UserSourceDatabase {
return nil // password is not required for non-database users, so no check needed
}
if u.Password == "" {
return nil // password is not set, so no check needed
}
if len(u.Password) < minLength {
return fmt.Errorf("password is too short, minimum length is %d", minLength)
}
return nil // password is strong enough
}
func (u *User) EditAllowed(new *User) error {
if u.Source == UserSourceDatabase {
return nil