summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxim Tkachenko <maxim.tkachenko@gmail.com>2019-10-14 22:24:26 +0700
committerzeripath <art27@cantab.net>2019-10-14 16:24:26 +0100
commitdb657192d0349f7b10a62515fbf085d3a48d88f9 (patch)
treed298b9b2c487af61dc399774e67dcb3440add9c2
parentf9aba9ba0f07b77cb46dde6eda3c3f5b8fa841fe (diff)
downloadgitea-db657192d0349f7b10a62515fbf085d3a48d88f9.tar.gz
gitea-db657192d0349f7b10a62515fbf085d3a48d88f9.zip
Password Complexity Checks (#6230)
Add password complexity checks. The default settings require a lowercase, uppercase, number and a special character within passwords. Co-Authored-By: T-M-A <maxim.tkachenko@gmail.com> Co-Authored-By: Lanre Adelowo <adelowomailbox@gmail.com> Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-Authored-By: Lauris BH <lauris@nix.lv>
-rw-r--r--cmd/admin.go19
-rw-r--r--custom/conf/app.ini.sample5
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md5
-rw-r--r--modules/password/password.go73
-rw-r--r--modules/setting/setting.go22
-rw-r--r--options/locale/locale_en-US.ini1
-rw-r--r--routers/admin/users.go10
-rw-r--r--routers/api/v1/admin/user.go14
-rw-r--r--routers/user/auth.go11
-rw-r--r--routers/user/setting/account.go3
-rw-r--r--routers/user/setting/account_test.go81
11 files changed, 207 insertions, 37 deletions
diff --git a/cmd/admin.go b/cmd/admin.go
index 4c4d6f9b66..4346159feb 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -13,9 +13,9 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/oauth2"
- "code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ pwd "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli"
@@ -233,7 +233,9 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(); err != nil {
return err
}
-
+ if !pwd.IsComplexEnough(c.String("password")) {
+ return errors.New("Password does not meet complexity requirements")
+ }
uname := c.String("username")
user, err := models.GetUserByName(uname)
if err != nil {
@@ -243,6 +245,7 @@ func runChangePassword(c *cli.Context) error {
return err
}
user.HashPassword(c.String("password"))
+
if err := models.UpdateUserCols(user, "passwd", "salt"); err != nil {
return err
}
@@ -275,26 +278,24 @@ func runCreateUser(c *cli.Context) error {
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
}
- var password string
+ if err := initDB(); err != nil {
+ return err
+ }
+ var password string
if c.IsSet("password") {
password = c.String("password")
} else if c.IsSet("random-password") {
var err error
- password, err = generate.GetRandomString(c.Int("random-password-length"))
+ password, err = pwd.Generate(c.Int("random-password-length"))
if err != nil {
return err
}
-
fmt.Printf("generated random password is '%s'\n", password)
} else {
return errors.New("must set either password or random-password flag")
}
- if err := initDB(); err != nil {
- return err
- }
-
// always default to true
var changePassword = true
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index fd8d928ede..79d9960052 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -332,6 +332,9 @@ MIN_PASSWORD_LENGTH = 6
IMPORT_LOCAL_PATHS = false
; Set to true to prevent all users (including admin) from creating custom git hooks
DISABLE_GIT_HOOKS = false
+;Comma separated list of character classes required to pass minimum complexity.
+;If left empty or no valid values are specified, the default values (`lower,upper,digit,spec`) will be used.
+PASSWORD_COMPLEXITY = lower,upper,digit,spec
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
PASSWORD_HASH_ALGO = pbkdf2
; Set false to allow JavaScript to read CSRF cookie
@@ -415,7 +418,7 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true
; Public is for everyone
DEFAULT_ORG_VISIBILITY = public
; Default value for DefaultOrgMemberVisible
-; True will make the membership of the users visible when added to the organisation
+; True will make the membership of the users visible when added to the organisation
DEFAULT_ORG_MEMBER_VISIBLE = false
; Default value for EnableDependencies
; Repositories will use dependencies by default depending on this setting
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index b927793a50..100bb229ee 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -208,6 +208,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\].
- `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
+- `PASSWORD_COMPLEXITY`: **lower,upper,digit,spec**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, the default values will be used. Possible values are:
+ - lower - use one or more lower latin characters
+ - upper - use one or more upper latin characters
+ - digit - use one or more digits
+ - spec - use one or more special characters as ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol.
## OpenID (`openid`)
diff --git a/modules/password/password.go b/modules/password/password.go
new file mode 100644
index 0000000000..54131b9641
--- /dev/null
+++ b/modules/password/password.go
@@ -0,0 +1,73 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package password
+
+import (
+ "crypto/rand"
+ "math/big"
+ "regexp"
+ "sync"
+
+ "code.gitea.io/gitea/modules/setting"
+)
+
+var matchComplexities = map[string]regexp.Regexp{}
+var matchComplexityOnce sync.Once
+var validChars string
+var validComplexities = map[string]string{
+ "lower": "abcdefghijklmnopqrstuvwxyz",
+ "upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+ "digit": "0123456789",
+ "spec": `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-",
+}
+
+// NewComplexity for preparation
+func NewComplexity() {
+ matchComplexityOnce.Do(func() {
+ if len(setting.PasswordComplexity) > 0 {
+ for key, val := range setting.PasswordComplexity {
+ matchComplexity := regexp.MustCompile(val)
+ matchComplexities[key] = *matchComplexity
+ validChars += validComplexities[key]
+ }
+ } else {
+ for _, val := range validComplexities {
+ validChars += val
+ }
+ }
+ })
+}
+
+// IsComplexEnough return True if password is Complexity
+func IsComplexEnough(pwd string) bool {
+ if len(setting.PasswordComplexity) > 0 {
+ NewComplexity()
+ for _, val := range matchComplexities {
+ if !val.MatchString(pwd) {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// Generate a random password
+func Generate(n int) (string, error) {
+ NewComplexity()
+ buffer := make([]byte, n)
+ max := big.NewInt(int64(len(validChars)))
+ for {
+ for j := 0; j < n; j++ {
+ rnd, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ return "", err
+ }
+ buffer[j] = validChars[rnd.Int64()]
+ }
+ if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
+ return string(buffer), nil
+ }
+ }
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 8c61bdbb77..278ed4b107 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -146,6 +146,7 @@ var (
MinPasswordLength int
ImportLocalPaths bool
DisableGitHooks bool
+ PasswordComplexity map[string]string
PasswordHashAlgo string
// UI settings
@@ -774,6 +775,27 @@ func NewContext() {
InternalToken = loadInternalToken(sec)
+ var dictPC = map[string]string{
+ "lower": "[a-z]+",
+ "upper": "[A-Z]+",
+ "digit": "[0-9]+",
+ "spec": `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-",
+ }
+ PasswordComplexity = make(map[string]string)
+ cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
+ for _, y := range cfgdata {
+ ts := strings.TrimSpace(y)
+ for a := range dictPC {
+ if strings.ToLower(ts) == a {
+ PasswordComplexity[ts] = dictPC[ts]
+ break
+ }
+ }
+ }
+ if len(PasswordComplexity) == 0 {
+ PasswordComplexity = dictPC
+ }
+
sec = Cfg.Section("attachment")
AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))
if !filepath.IsAbs(AttachmentPath) {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e6c5839a64..4a92b08030 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -315,6 +315,7 @@ team_no_units_error = Allow access to at least one repository section.
email_been_used = The email address is already used.
openid_been_used = The OpenID address '%s' is already used.
username_password_incorrect = Username or password is incorrect.
+password_complexity = Password does not pass complexity requirements.
enterred_invalid_repo_name = The repository name you entered is incorrect.
enterred_invalid_owner_name = The new owner name is not valid.
enterred_invalid_password = The password you entered is incorrect.
diff --git a/routers/admin/users.go b/routers/admin/users.go
index 660f116682..fdc4e0e371 100644
--- a/routers/admin/users.go
+++ b/routers/admin/users.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/services/mailer"
@@ -94,7 +95,10 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) {
u.LoginName = form.LoginName
}
}
-
+ if !password.IsComplexEnough(form.Password) {
+ ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserNew, &form)
+ return
+ }
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
@@ -201,6 +205,10 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
ctx.ServerError("UpdateUser", err)
return
}
+ if !password.IsComplexEnough(form.Password) {
+ ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserEdit, &form)
+ return
+ }
u.HashPassword(form.Password)
}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 70076b626b..f35ad297b0 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -6,9 +6,12 @@
package admin
import (
+ "errors"
+
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/password"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/convert"
"code.gitea.io/gitea/routers/api/v1/user"
@@ -73,7 +76,11 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
if ctx.Written() {
return
}
-
+ if !password.IsComplexEnough(form.Password) {
+ err := errors.New("PasswordComplexity")
+ ctx.Error(400, "PasswordComplexity", err)
+ return
+ }
if err := models.CreateUser(u); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrEmailAlreadyUsed(err) ||
@@ -131,6 +138,11 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) {
}
if len(form.Password) > 0 {
+ if !password.IsComplexEnough(form.Password) {
+ err := errors.New("PasswordComplexity")
+ ctx.Error(400, "PasswordComplexity", err)
+ return
+ }
var err error
if u.Salt, err = models.GetUserSalt(); err != nil {
ctx.Error(500, "UpdateUser", err)
diff --git a/routers/user/auth.go b/routers/user/auth.go
index 212d535a06..82a508e4dc 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -1334,6 +1335,11 @@ func ResetPasswdPost(ctx *context.Context) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
return
+ } else if !password.IsComplexEnough(passwd) {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplResetPassword, nil)
+ return
}
var err error
@@ -1364,7 +1370,6 @@ func ResetPasswdPost(ctx *context.Context) {
func MustChangePassword(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
-
ctx.HTML(200, tplMustChangePassword)
}
@@ -1372,16 +1377,12 @@ func MustChangePassword(ctx *context.Context) {
// account was created by an admin
func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form auth.MustChangePasswordForm) {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
-
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
-
if ctx.HasError() {
ctx.HTML(200, tplMustChangePassword)
return
}
-
u := ctx.User
-
// Make sure only requests for users who are eligible to change their password via
// this method passes through
if !u.MustChangePassword {
diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go
index 71d98fd3b9..c782224216 100644
--- a/routers/user/setting/account.go
+++ b/routers/user/setting/account.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/services/mailer"
@@ -52,6 +53,8 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
+ } else if !password.IsComplexEnough(form.Password) {
+ ctx.Flash.Error(ctx.Tr("settings.password_complexity"))
} else {
var err error
if ctx.User.Salt, err = models.GetUserSalt(); err != nil {
diff --git a/routers/user/setting/account_test.go b/routers/user/setting/account_test.go
index 59fbda1569..497ee658b0 100644
--- a/routers/user/setting/account_test.go
+++ b/routers/user/setting/account_test.go
@@ -19,36 +19,77 @@ import (
func TestChangePassword(t *testing.T) {
oldPassword := "password"
setting.MinPasswordLength = 6
+ setting.PasswordComplexity = map[string]string{
+ "lower": "[a-z]+",
+ "upper": "[A-Z]+",
+ "digit": "[0-9]+",
+ "spec": "[-_]+",
+ }
+ var pcLUN = map[string]string{
+ "lower": "[a-z]+",
+ "upper": "[A-Z]+",
+ "digit": "[0-9]+",
+ }
+ var pcLU = map[string]string{
+ "lower": "[a-z]+",
+ "upper": "[A-Z]+",
+ }
for _, req := range []struct {
- OldPassword string
- NewPassword string
- Retype string
- Message string
+ OldPassword string
+ NewPassword string
+ Retype string
+ Message string
+ PasswordComplexity map[string]string
}{
{
- OldPassword: oldPassword,
- NewPassword: "123456",
- Retype: "123456",
- Message: "",
+ OldPassword: oldPassword,
+ NewPassword: "Qwerty123456-",
+ Retype: "Qwerty123456-",
+ Message: "",
+ PasswordComplexity: setting.PasswordComplexity,
+ },
+ {
+ OldPassword: oldPassword,
+ NewPassword: "12345",
+ Retype: "12345",
+ Message: "auth.password_too_short",
+ PasswordComplexity: setting.PasswordComplexity,
+ },
+ {
+ OldPassword: "12334",
+ NewPassword: "123456",
+ Retype: "123456",
+ Message: "settings.password_incorrect",
+ PasswordComplexity: setting.PasswordComplexity,
+ },
+ {
+ OldPassword: oldPassword,
+ NewPassword: "123456",
+ Retype: "12345",
+ Message: "form.password_not_match",
+ PasswordComplexity: setting.PasswordComplexity,
},
{
- OldPassword: oldPassword,
- NewPassword: "12345",
- Retype: "12345",
- Message: "auth.password_too_short",
+ OldPassword: oldPassword,
+ NewPassword: "Qwerty",
+ Retype: "Qwerty",
+ Message: "settings.password_complexity",
+ PasswordComplexity: setting.PasswordComplexity,
},
{
- OldPassword: "12334",
- NewPassword: "123456",
- Retype: "123456",
- Message: "settings.password_incorrect",
+ OldPassword: oldPassword,
+ NewPassword: "Qwerty",
+ Retype: "Qwerty",
+ Message: "settings.password_complexity",
+ PasswordComplexity: pcLUN,
},
{
- OldPassword: oldPassword,
- NewPassword: "123456",
- Retype: "12345",
- Message: "form.password_not_match",
+ OldPassword: oldPassword,
+ NewPassword: "QWERTY",
+ Retype: "QWERTY",
+ Message: "settings.password_complexity",
+ PasswordComplexity: pcLU,
},
} {
models.PrepareTestEnv(t)