Add support for min upper/lower-case password constraints
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user