Pull Request Comment Resolutions:

* Change checking symbols / upper / lower from regex to using the
`unicode` package to be more non-English-language-friendly.

* Name fields in validatePassword test to make it easier to read

* Adding a test for the defaults for PasswordRequirements

Signed-off-by: Devon Mizelle <dev@devon.so>
This commit is contained in:
Devon Mizelle 2021-08-17 11:31:16 -04:00
parent 6cfb0a36d7
commit e1c7ad2675
3 changed files with 95 additions and 75 deletions

View file

@ -28,6 +28,7 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode"
"github.com/matrix-org/dendrite/internal/eventutil" "github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
@ -109,10 +110,6 @@ var (
// sessions stores the completed flow stages for all sessions. Referenced using their sessionID. // sessions stores the completed flow stages for all sessions. Referenced using their sessionID.
sessions = newSessionsDict() sessions = newSessionsDict()
validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-=./]+$`) validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-=./]+$`)
passwordSymbols = regexp.MustCompile(`[^0-9a-zA-Z]`)
passwordUppercase = regexp.MustCompile(`[A-Z]`)
passwordLowercase = regexp.MustCompile(`[a-z]`)
) )
// registerRequest represents the submitted registration request. // registerRequest represents the submitted registration request.
@ -229,38 +226,49 @@ func validateApplicationServiceUsername(username string) *util.JSONResponse {
// validatePassword returns an error response if the password is invalid // validatePassword returns an error response if the password is invalid
func validatePassword(password string, cfg config.PasswordRequirements) *util.JSONResponse { func validatePassword(password string, cfg config.PasswordRequirements) *util.JSONResponse {
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
if len(password) > cfg.MaxPasswordLength { if len(password) > cfg.MaxPasswordLength {
return &util.JSONResponse{ return &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("'password' >%d characters", cfg.MaxPasswordLength)), JSON: jsonerror.WeakPassword(fmt.Sprintf("'password' >%d characters", cfg.MaxPasswordLength)),
} }
} else if len(password) > 0 && len(password) < cfg.MinPasswordLength { } else if len(password) >= 0 && len(password) < cfg.MinPasswordLength {
return &util.JSONResponse{ return &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", cfg.MinPasswordLength)), JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", cfg.MinPasswordLength)),
} }
} }
if cfg.MinNumberSymbols > 0 { var (
matches := passwordSymbols.FindAllStringIndex(password, -1) symbolCount = 0
if len(matches) < cfg.MinNumberSymbols { uppercaseCount = 0
lowercaseCount = 0
)
for _, letter := range password {
switch {
case unicode.IsSymbol(letter) || unicode.IsPunct(letter):
symbolCount++
case unicode.IsUpper(letter):
uppercaseCount++
case unicode.IsLower(letter):
lowercaseCount++
}
}
if cfg.MinNumberSymbols > 0 && symbolCount < cfg.MinNumberSymbols {
return &util.JSONResponse{ return &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: minimum %d symbols", cfg.MinNumberSymbols)), JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: minimum %d symbols", cfg.MinNumberSymbols)),
} }
} }
}
if cfg.RequireMixedCase { if cfg.RequireMixedCase && (uppercaseCount == 0 || lowercaseCount == 0) {
lowercase := passwordLowercase.FindAllStringIndex(password, -1)
uppercase := passwordUppercase.FindAllStringIndex(password, -1)
if len(lowercase) == 0 || len(uppercase) == 0 {
return &util.JSONResponse{ return &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword("password must have uppercase and lowercase letters"), JSON: jsonerror.WeakPassword("password must have uppercase and lowercase letters"),
} }
} }
}
return nil return nil
} }

View file

@ -15,9 +15,9 @@
package routing package routing
import ( import (
"fmt"
"net/http" "net/http"
"regexp" "regexp"
"strings"
"testing" "testing"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
@ -231,78 +231,66 @@ func TestValidatePassword(t *testing.T) {
expected *util.JSONResponse expected *util.JSONResponse
}{ }{
{ {
"default reject too short", name: "default reject too short",
*defaults, config: *defaults,
"foobar", password: strings.Repeat("a", defaults.MinPasswordLength-1),
&util.JSONResponse{ expected: &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", defaults.MinPasswordLength)),
}, },
}, },
{ {
"default reject too long", name: "default reject too long",
*defaults, config: *defaults,
// len 600 password: strings.Repeat("a", defaults.MaxPasswordLength+10),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expected: &util.JSONResponse{
&util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("'password' >%d characters", defaults.MaxPasswordLength)),
}, },
}, },
{ {
"default allow long enough", name: "default reject empty password",
*defaults, config: *defaults,
"thisisalongenoughpassword", password: "",
nil, expected: &util.JSONResponse{
},
{
"default allow with symbols",
*defaults,
"ih@ve$ome$ymbol$_here",
nil,
},
{
"set min reject too short",
*custom,
"abcd",
&util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", custom.MinPasswordLength)),
}, },
}, },
{ {
"set max reject too long", name: "set min reject too short",
*custom, config: *custom,
// len 33 password: "abcd",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", expected: &util.JSONResponse{
&util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("'password' >%d characters", custom.MaxPasswordLength)),
}, },
}, },
{ {
"set symbols not enough", name: "set max reject too long",
*custom, config: *custom,
"thi$i$apasswordshouldbelong", password: strings.Repeat("x", custom.MaxPasswordLength+10),
&util.JSONResponse{ expected: &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: minimum %d symbols", custom.MinNumberSymbols)),
}, },
}, },
{ {
"require mixed case but none given", name: "set symbols not enough",
*custom, config: *custom,
"haha_all_lowercase_cant_catch_me", password: "thi$i$apasswordshouldbelong",
&util.JSONResponse{ expected: &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.WeakPassword("password must have uppercase and lowercase letters"),
}, },
}, },
{ {
"custom settings allow", name: "require mixed case but none given",
*custom, config: *custom,
"$0me_$up3r_$trong_P@ass", password: "haha_all_lowercase_cant_catch_me",
nil, expected: &util.JSONResponse{
Code: http.StatusBadRequest,
},
},
{
name: "custom settings allow",
config: *custom,
password: "$0me_$up3r_$trong_P@ass",
expected: nil,
}, },
} }
@ -317,13 +305,15 @@ func TestValidatePassword(t *testing.T) {
t.Errorf("expected password to fail, but was validated") t.Errorf("expected password to fail, but was validated")
} else if test.expected != nil && test.expected.Code != response.Code { } else if test.expected != nil && test.expected.Code != response.Code {
t.Errorf("expected error code %d, got %d", test.expected.Code, response.Code) t.Errorf("expected error code %d, got %d", test.expected.Code, response.Code)
} else if test.expected != nil && response != nil { // } else if test.expected != nil && response != nil {
matrixError := response.JSON.(*jsonerror.MatrixError) // matrixError := response.JSON.(*jsonerror.MatrixError)
expectedError := test.expected.JSON.(*jsonerror.MatrixError) // expectedError := test.expected.JSON.(*jsonerror.MatrixError)
if expectedError.Err != matrixError.Err { // if expectedError.Err != matrixError.Err {
t.Errorf("expected error: %s, got error: %s", expectedError.Err, matrixError.Err) // t.Errorf("expected error: %s, got error: %s", expectedError.Err, matrixError.Err)
} // }
} else if test.expected != nil && test.expected.Code == response.Code {
t.Logf("expected error code %d matches %d", test.expected.Code, response.Code)
} else if test.expected == nil && response == nil { } else if test.expected == nil && response == nil {
t.Log("password validation passed") t.Log("password validation passed")
} else { } else {

View file

@ -0,0 +1,22 @@
package config
import (
"testing"
)
func TestPasswordRequirementsDefaults(t *testing.T) {
requirements := &PasswordRequirements{}
requirements.Defaults()
if requirements.MinPasswordLength <= 0 {
t.Errorf("default minimum password length should be greater than 0, got %d", requirements.MinPasswordLength)
}
if requirements.MinPasswordLength > requirements.MaxPasswordLength {
t.Errorf(
"default minimum password length should not be greater than maxmium password length. currently: %d > %d",
requirements.MinPasswordLength,
requirements.MaxPasswordLength,
)
}
}