Support leading and trailing spaces in user passwords

This improves compatibility with external authentication providers that
allow such characters in passwords.

Passwords created via the WebAdmin UI are still sanitized to prevent user
confusion.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2025-04-26 14:29:36 +02:00
parent a709b84eef
commit 9e2230cc33
4 changed files with 56 additions and 4 deletions

View File

@@ -366,6 +366,7 @@ func (a *Admin) validateGroups() error {
func (a *Admin) validate() error { func (a *Admin) validate() error {
a.SetEmptySecretsIfNil() a.SetEmptySecretsIfNil()
a.Password = strings.TrimSpace(a.Password)
if a.Username == "" { if a.Username == "" {
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired) return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
} }
@@ -481,7 +482,7 @@ func (a *Admin) checkUserAndPass(password, ip string) error {
if err := a.CanLogin(ip); err != nil { if err := a.CanLogin(ip); err != nil {
return err return err
} }
if a.Password == "" || password == "" { if a.Password == "" || strings.TrimSpace(password) == "" {
return errors.New("credentials cannot be null or empty") return errors.New("credentials cannot be null or empty")
} }
match, err := a.CheckPassword(password) match, err := a.CheckPassword(password)

View File

@@ -3537,7 +3537,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if err != nil { if err != nil {
return *user, ErrInvalidCredentials return *user, ErrInvalidCredentials
} }
if user.Password == "" || password == "" { if user.Password == "" || strings.TrimSpace(password) == "" {
return *user, errors.New("credentials cannot be null or empty") return *user, errors.New("credentials cannot be null or empty")
} }
if !user.Filters.Hooks.CheckPasswordDisabled { if !user.Filters.Hooks.CheckPasswordDisabled {

View File

@@ -6908,6 +6908,57 @@ func TestAdminGenerateRecoveryCodesSaveError(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestAdminCredentialsWithSpaces(t *testing.T) {
a := getTestAdmin()
a.Username = xid.New().String()
a.Password = " " + xid.New().String() + " "
admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
assert.NoError(t, err)
// For admins the password is always trimmed.
_, err = getJWTAPITokenFromTestServer(a.Username, a.Password)
assert.Error(t, err)
_, err = getJWTAPITokenFromTestServer(a.Username, strings.TrimSpace(a.Password))
assert.NoError(t, err)
// The password sent from the WebAdmin UI is automatically trimmed
_, err = getJWTWebToken(a.Username, a.Password)
assert.NoError(t, err)
_, err = getJWTWebToken(a.Username, strings.TrimSpace(a.Password))
assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
}
func TestUserCredentialsWithSpaces(t *testing.T) {
u := getTestUser()
u.Password = " " + xid.New().String() + " "
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
// For users the password is not trimmed
_, err = getJWTAPIUserTokenFromTestServer(u.Username, u.Password)
assert.NoError(t, err)
_, err = getJWTAPIUserTokenFromTestServer(u.Username, strings.TrimSpace(u.Password))
assert.Error(t, err)
_, err = getJWTWebClientTokenFromTestServer(u.Username, u.Password)
assert.NoError(t, err)
_, err = getJWTWebClientTokenFromTestServer(u.Username, strings.TrimSpace(u.Password))
assert.Error(t, err)
user.Password = u.Password
conn, sftpClient, err := getSftpClient(user)
if assert.NoError(t, err) {
conn.Close()
sftpClient.Close()
}
user.Password = strings.TrimSpace(u.Password)
_, _, err = getSftpClient(user)
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestNamingRules(t *testing.T) { func TestNamingRules(t *testing.T) {
smtpCfg := smtp.Config{ smtpCfg := smtp.Config{
Host: "127.0.0.1", Host: "127.0.0.1",

View File

@@ -244,7 +244,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
} }
protocol := common.ProtocolHTTP protocol := common.ProtocolHTTP
username := strings.TrimSpace(r.Form.Get("username")) username := strings.TrimSpace(r.Form.Get("username"))
password := strings.TrimSpace(r.Form.Get("password")) password := r.Form.Get("password")
if username == "" || password == "" { if username == "" || password == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r) dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)
@@ -840,7 +840,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
if username == "" || password == "" { if username == "" || strings.TrimSpace(password) == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r) dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)