From 507e5cd72f1a4e43145febf2b853f396fa517802 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 1 Aug 2022 16:37:46 +0200 Subject: [PATCH] Add support for min upper/lower-case password constraints --- system/service/auth_credentials.go | 97 ++++++++++++++++--------- system/service/auth_credentials_test.go | 91 +++++++++++++++++++++++ system/types/app_settings.go | 34 +++++---- 3 files changed, 175 insertions(+), 47 deletions(-) diff --git a/system/service/auth_credentials.go b/system/service/auth_credentials.go index c9c494fde..374f5a2a8 100644 --- a/system/service/auth_credentials.go +++ b/system/service/auth_credentials.go @@ -16,7 +16,6 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/dgryski/dgoogauth" "golang.org/x/crypto/bcrypt" - "math" rand2 "math/rand" "regexp" "sort" @@ -528,39 +527,7 @@ func (svc *auth) hashPassword(password string) (hash []byte, err error) { } func (svc *auth) CheckPasswordStrength(password string) bool { - pwdL := len(password) - - // Ignore defined password constraints - if !svc.settings.Auth.Internal.PasswordConstraints.PasswordSecurity { - return true - } - - // Check the password length - minL := math.Max(float64(passwordMinLength), float64(svc.settings.Auth.Internal.PasswordConstraints.MinLength)) - if pwdL < int(minL) || pwdL > passwordMaxLength { - return false - } - - // Check special constraints - // - numeric characters - count := svc.settings.Auth.Internal.PasswordConstraints.MinNumCount - if count > 0 { - rr := regexp.MustCompile("[0-9]") - if uint(len(rr.FindAllStringIndex(password, -1))) < count { - return false - } - } - - // - special characters - count = svc.settings.Auth.Internal.PasswordConstraints.MinSpecialCount - if count > 0 { - rr := regexp.MustCompile("[^0-9a-zA-Z]") - if uint(len(rr.FindAllStringIndex(password, -1))) < count { - return false - } - } - - return true + return checkPasswordStrength(password, svc.settings.Auth.Internal.PasswordConstraints) } // SetPasswordCredentials (soft) deletes old password entry and creates a new entry with new password on every change @@ -1148,3 +1115,65 @@ func credentialsFilter(cc []*types.Credential, limit int, mm ...func(*types.Cred return } + +func checkPasswordStrength(password string, pc types.PasswordConstraints) bool { + var ( + length = len(password) + re *regexp.Regexp + mt [][]int + ) + + // Always check system constraints + if length < passwordMinLength || length > passwordMaxLength { + return false + } + + // Ignore defined password constraints + if !pc.PasswordSecurity { + return true + } + + // Check the password length + if length < int(pc.MinLength) { + return false + } + + // Check special constraints + // - numeric characters + if count := int(pc.MinNumCount); count > 0 { + re = regexp.MustCompile("[0-9]") + mt = re.FindAllStringIndex(password, -1) + if len(mt) < count { + return false + } + } + + // Check for lowercase characters + if count := int(pc.MinLowerCase); count > 0 { + re = regexp.MustCompile("[a-z]") + mt = re.FindAllStringIndex(password, -1) + if len(mt) < count { + return false + } + } + + // Check for upper-case characters + if count := int(pc.MinUpperCase); count > 0 { + re = regexp.MustCompile("[A-Z]") + mt = re.FindAllStringIndex(password, -1) + if len(mt) < count { + return false + } + } + + // - special characters + if count := int(pc.MinSpecialCount); count > 0 { + re = regexp.MustCompile("[^0-9a-zA-Z]") + mt = re.FindAllStringIndex(password, -1) + if len(mt) < count { + return false + } + } + + return true +} diff --git a/system/service/auth_credentials_test.go b/system/service/auth_credentials_test.go index 1023a32bd..e64de1b6d 100644 --- a/system/service/auth_credentials_test.go +++ b/system/service/auth_credentials_test.go @@ -4,6 +4,7 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" + "strings" "testing" "time" ) @@ -196,3 +197,93 @@ func TestValidateToken(t *testing.T) { }) } } + +func Test_checkPasswordStrength(t *testing.T) { + tests := []struct { + name string + pc types.PasswordConstraints + password string + want bool + }{ + { + name: "empty", + pc: types.PasswordConstraints{}, + password: "", + want: false, + }, + { + name: "sys too short", + pc: types.PasswordConstraints{}, + password: strings.Repeat("A", passwordMinLength-1), + want: false, + }, + { + name: "sys too long", + pc: types.PasswordConstraints{}, + password: strings.Repeat("A", passwordMaxLength+1), + want: false, + }, + { + name: "too short", + pc: types.PasswordConstraints{MinLength: 10}, + password: "123456789", + want: false, + }, + { + name: "uc valid", + pc: types.PasswordConstraints{MinUpperCase: 2}, + password: "aaaAAAaaa", + want: true, + }, + { + name: "uc invalid", + pc: types.PasswordConstraints{MinUpperCase: 2}, + password: "aaaaaaaa", + want: false, + }, + { + name: "lc valid", + pc: types.PasswordConstraints{MinLowerCase: 2}, + password: "AAAaaAAAA", + want: true, + }, + { + name: "lc invalid", + pc: types.PasswordConstraints{MinLowerCase: 2}, + password: "AAAAAAAAA", + want: false, + }, + { + name: "digit valid", + pc: types.PasswordConstraints{MinNumCount: 2}, + password: "AAA12AAAA", + want: true, + }, + { + name: "digit invalid", + pc: types.PasswordConstraints{MinNumCount: 2}, + password: "AAAaaAAAA", + want: false, + }, + { + name: "special valid", + pc: types.PasswordConstraints{MinSpecialCount: 2}, + password: "AAA!!AAAA", + want: true, + }, + { + name: "special invalid", + pc: types.PasswordConstraints{MinSpecialCount: 2}, + password: "AAAaaAAAA", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pc.PasswordSecurity = true + if got := checkPasswordStrength(tt.password, tt.pc); got != tt.want { + t.Errorf("checkPasswordStrength() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/system/types/app_settings.go b/system/types/app_settings.go index 498264d50..a4158b8af 100644 --- a/system/types/app_settings.go +++ b/system/types/app_settings.go @@ -69,19 +69,7 @@ type ( // If only one ext. provider is enabled, user is automatically redirected there SplitCredentialsCheck bool `json:"-" kv:"split-credentials-check"` - PasswordConstraints struct { - // Should the environment not enforce the constraints - PasswordSecurity bool `kv:"-" json:"passwordSecurity"` - - // The min password length - MinLength uint `kv:"min-length"` - - // The min number of numeric characters - MinNumCount uint `kv:"min-num-count"` - - // The min number of special characters - MinSpecialCount uint `kv:"min-special-count"` - } `kv:"password-constraints" json:"passwordConstraints"` + PasswordConstraints PasswordConstraints `kv:"password-constraints" json:"passwordConstraints"` } `json:"internal"` External struct { @@ -385,6 +373,26 @@ type ( TlsInsecure bool `json:"tlsInsecure"` TlsServerName string `json:"tlsServerName"` } + + PasswordConstraints struct { + // Should the environment not enforce the constraints + PasswordSecurity bool `kv:"-" json:"passwordSecurity"` + + // The min password length + MinLength uint `kv:"min-length"` + + // Minimum number of uppercase letters in password + MinUpperCase uint `kv:"min-upper-case"` + + // Minimum number of lowercase letters in password + MinLowerCase uint `kv:"min-lower-case"` + + // The min number of numeric characters + MinNumCount uint `kv:"min-num-count"` + + // The min number of special characters + MinSpecialCount uint `kv:"min-special-count"` + } ) // WithDefaults sets defaults on copy (!!) of settings