Skip to content

Commit

Permalink
Password Complexity Checks (go-gitea#6230)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
4 people authored and zeripath committed Oct 14, 2019
1 parent f9aba9b commit db65719
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 37 deletions.
19 changes: 10 additions & 9 deletions cmd/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
73 changes: 73 additions & 0 deletions modules/password/password.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
22 changes: 22 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ var (
MinPasswordLength int
ImportLocalPaths bool
DisableGitHooks bool
PasswordComplexity map[string]string
PasswordHashAlgo string

// UI settings
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion routers/admin/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
}

Expand Down
14 changes: 13 additions & 1 deletion routers/api/v1/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 6 additions & 5 deletions routers/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1364,24 +1370,19 @@ 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)
}

// MustChangePasswordPost response for updating a user's password after his/her
// 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 {
Expand Down
3 changes: 3 additions & 0 deletions routers/user/setting/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit db65719

Please sign in to comment.