Skip to content

Commit db65719

Browse files
T-M-Aadelowoguillep2klafriks
authored andcommitted
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 <[email protected]> Co-Authored-By: Lanre Adelowo <[email protected]> Co-Authored-By: guillep2k <[email protected]> Co-Authored-By: Lauris BH <[email protected]>
1 parent f9aba9b commit db65719

File tree

11 files changed

+207
-37
lines changed

11 files changed

+207
-37
lines changed

cmd/admin.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import (
1313

1414
"code.gitea.io/gitea/models"
1515
"code.gitea.io/gitea/modules/auth/oauth2"
16-
"code.gitea.io/gitea/modules/generate"
1716
"code.gitea.io/gitea/modules/git"
1817
"code.gitea.io/gitea/modules/log"
18+
pwd "code.gitea.io/gitea/modules/password"
1919
"code.gitea.io/gitea/modules/setting"
2020

2121
"github.com/urfave/cli"
@@ -233,7 +233,9 @@ func runChangePassword(c *cli.Context) error {
233233
if err := initDB(); err != nil {
234234
return err
235235
}
236-
236+
if !pwd.IsComplexEnough(c.String("password")) {
237+
return errors.New("Password does not meet complexity requirements")
238+
}
237239
uname := c.String("username")
238240
user, err := models.GetUserByName(uname)
239241
if err != nil {
@@ -243,6 +245,7 @@ func runChangePassword(c *cli.Context) error {
243245
return err
244246
}
245247
user.HashPassword(c.String("password"))
248+
246249
if err := models.UpdateUserCols(user, "passwd", "salt"); err != nil {
247250
return err
248251
}
@@ -275,26 +278,24 @@ func runCreateUser(c *cli.Context) error {
275278
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
276279
}
277280

278-
var password string
281+
if err := initDB(); err != nil {
282+
return err
283+
}
279284

285+
var password string
280286
if c.IsSet("password") {
281287
password = c.String("password")
282288
} else if c.IsSet("random-password") {
283289
var err error
284-
password, err = generate.GetRandomString(c.Int("random-password-length"))
290+
password, err = pwd.Generate(c.Int("random-password-length"))
285291
if err != nil {
286292
return err
287293
}
288-
289294
fmt.Printf("generated random password is '%s'\n", password)
290295
} else {
291296
return errors.New("must set either password or random-password flag")
292297
}
293298

294-
if err := initDB(); err != nil {
295-
return err
296-
}
297-
298299
// always default to true
299300
var changePassword = true
300301

custom/conf/app.ini.sample

+4-1
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ MIN_PASSWORD_LENGTH = 6
332332
IMPORT_LOCAL_PATHS = false
333333
; Set to true to prevent all users (including admin) from creating custom git hooks
334334
DISABLE_GIT_HOOKS = false
335+
;Comma separated list of character classes required to pass minimum complexity.
336+
;If left empty or no valid values are specified, the default values (`lower,upper,digit,spec`) will be used.
337+
PASSWORD_COMPLEXITY = lower,upper,digit,spec
335338
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
336339
PASSWORD_HASH_ALGO = pbkdf2
337340
; Set false to allow JavaScript to read CSRF cookie
@@ -415,7 +418,7 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true
415418
; Public is for everyone
416419
DEFAULT_ORG_VISIBILITY = public
417420
; Default value for DefaultOrgMemberVisible
418-
; True will make the membership of the users visible when added to the organisation
421+
; True will make the membership of the users visible when added to the organisation
419422
DEFAULT_ORG_MEMBER_VISIBLE = false
420423
; Default value for EnableDependencies
421424
; Repositories will use dependencies by default depending on this setting

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+5
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
208208
- `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`)
209209
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\].
210210
- `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
211+
- `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:
212+
- lower - use one or more lower latin characters
213+
- upper - use one or more upper latin characters
214+
- digit - use one or more digits
215+
- spec - use one or more special characters as ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol.
211216

212217
## OpenID (`openid`)
213218

modules/password/password.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package password
6+
7+
import (
8+
"crypto/rand"
9+
"math/big"
10+
"regexp"
11+
"sync"
12+
13+
"code.gitea.io/gitea/modules/setting"
14+
)
15+
16+
var matchComplexities = map[string]regexp.Regexp{}
17+
var matchComplexityOnce sync.Once
18+
var validChars string
19+
var validComplexities = map[string]string{
20+
"lower": "abcdefghijklmnopqrstuvwxyz",
21+
"upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
22+
"digit": "0123456789",
23+
"spec": `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-",
24+
}
25+
26+
// NewComplexity for preparation
27+
func NewComplexity() {
28+
matchComplexityOnce.Do(func() {
29+
if len(setting.PasswordComplexity) > 0 {
30+
for key, val := range setting.PasswordComplexity {
31+
matchComplexity := regexp.MustCompile(val)
32+
matchComplexities[key] = *matchComplexity
33+
validChars += validComplexities[key]
34+
}
35+
} else {
36+
for _, val := range validComplexities {
37+
validChars += val
38+
}
39+
}
40+
})
41+
}
42+
43+
// IsComplexEnough return True if password is Complexity
44+
func IsComplexEnough(pwd string) bool {
45+
if len(setting.PasswordComplexity) > 0 {
46+
NewComplexity()
47+
for _, val := range matchComplexities {
48+
if !val.MatchString(pwd) {
49+
return false
50+
}
51+
}
52+
}
53+
return true
54+
}
55+
56+
// Generate a random password
57+
func Generate(n int) (string, error) {
58+
NewComplexity()
59+
buffer := make([]byte, n)
60+
max := big.NewInt(int64(len(validChars)))
61+
for {
62+
for j := 0; j < n; j++ {
63+
rnd, err := rand.Int(rand.Reader, max)
64+
if err != nil {
65+
return "", err
66+
}
67+
buffer[j] = validChars[rnd.Int64()]
68+
}
69+
if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
70+
return string(buffer), nil
71+
}
72+
}
73+
}

modules/setting/setting.go

+22
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ var (
146146
MinPasswordLength int
147147
ImportLocalPaths bool
148148
DisableGitHooks bool
149+
PasswordComplexity map[string]string
149150
PasswordHashAlgo string
150151

151152
// UI settings
@@ -774,6 +775,27 @@ func NewContext() {
774775

775776
InternalToken = loadInternalToken(sec)
776777

778+
var dictPC = map[string]string{
779+
"lower": "[a-z]+",
780+
"upper": "[A-Z]+",
781+
"digit": "[0-9]+",
782+
"spec": `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-",
783+
}
784+
PasswordComplexity = make(map[string]string)
785+
cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
786+
for _, y := range cfgdata {
787+
ts := strings.TrimSpace(y)
788+
for a := range dictPC {
789+
if strings.ToLower(ts) == a {
790+
PasswordComplexity[ts] = dictPC[ts]
791+
break
792+
}
793+
}
794+
}
795+
if len(PasswordComplexity) == 0 {
796+
PasswordComplexity = dictPC
797+
}
798+
777799
sec = Cfg.Section("attachment")
778800
AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))
779801
if !filepath.IsAbs(AttachmentPath) {

options/locale/locale_en-US.ini

+1
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ team_no_units_error = Allow access to at least one repository section.
315315
email_been_used = The email address is already used.
316316
openid_been_used = The OpenID address '%s' is already used.
317317
username_password_incorrect = Username or password is incorrect.
318+
password_complexity = Password does not pass complexity requirements.
318319
enterred_invalid_repo_name = The repository name you entered is incorrect.
319320
enterred_invalid_owner_name = The new owner name is not valid.
320321
enterred_invalid_password = The password you entered is incorrect.

routers/admin/users.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/modules/base"
1313
"code.gitea.io/gitea/modules/context"
1414
"code.gitea.io/gitea/modules/log"
15+
"code.gitea.io/gitea/modules/password"
1516
"code.gitea.io/gitea/modules/setting"
1617
"code.gitea.io/gitea/routers"
1718
"code.gitea.io/gitea/services/mailer"
@@ -94,7 +95,10 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) {
9495
u.LoginName = form.LoginName
9596
}
9697
}
97-
98+
if !password.IsComplexEnough(form.Password) {
99+
ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserNew, &form)
100+
return
101+
}
98102
if err := models.CreateUser(u); err != nil {
99103
switch {
100104
case models.IsErrUserAlreadyExist(err):
@@ -201,6 +205,10 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
201205
ctx.ServerError("UpdateUser", err)
202206
return
203207
}
208+
if !password.IsComplexEnough(form.Password) {
209+
ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserEdit, &form)
210+
return
211+
}
204212
u.HashPassword(form.Password)
205213
}
206214

routers/api/v1/admin/user.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
package admin
77

88
import (
9+
"errors"
10+
911
"code.gitea.io/gitea/models"
1012
"code.gitea.io/gitea/modules/context"
1113
"code.gitea.io/gitea/modules/log"
14+
"code.gitea.io/gitea/modules/password"
1215
api "code.gitea.io/gitea/modules/structs"
1316
"code.gitea.io/gitea/routers/api/v1/convert"
1417
"code.gitea.io/gitea/routers/api/v1/user"
@@ -73,7 +76,11 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
7376
if ctx.Written() {
7477
return
7578
}
76-
79+
if !password.IsComplexEnough(form.Password) {
80+
err := errors.New("PasswordComplexity")
81+
ctx.Error(400, "PasswordComplexity", err)
82+
return
83+
}
7784
if err := models.CreateUser(u); err != nil {
7885
if models.IsErrUserAlreadyExist(err) ||
7986
models.IsErrEmailAlreadyUsed(err) ||
@@ -131,6 +138,11 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) {
131138
}
132139

133140
if len(form.Password) > 0 {
141+
if !password.IsComplexEnough(form.Password) {
142+
err := errors.New("PasswordComplexity")
143+
ctx.Error(400, "PasswordComplexity", err)
144+
return
145+
}
134146
var err error
135147
if u.Salt, err = models.GetUserSalt(); err != nil {
136148
ctx.Error(500, "UpdateUser", err)

routers/user/auth.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/modules/base"
1818
"code.gitea.io/gitea/modules/context"
1919
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/password"
2021
"code.gitea.io/gitea/modules/recaptcha"
2122
"code.gitea.io/gitea/modules/setting"
2223
"code.gitea.io/gitea/modules/timeutil"
@@ -1334,6 +1335,11 @@ func ResetPasswdPost(ctx *context.Context) {
13341335
ctx.Data["Err_Password"] = true
13351336
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
13361337
return
1338+
} else if !password.IsComplexEnough(passwd) {
1339+
ctx.Data["IsResetForm"] = true
1340+
ctx.Data["Err_Password"] = true
1341+
ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplResetPassword, nil)
1342+
return
13371343
}
13381344

13391345
var err error
@@ -1364,24 +1370,19 @@ func ResetPasswdPost(ctx *context.Context) {
13641370
func MustChangePassword(ctx *context.Context) {
13651371
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
13661372
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
1367-
13681373
ctx.HTML(200, tplMustChangePassword)
13691374
}
13701375

13711376
// MustChangePasswordPost response for updating a user's password after his/her
13721377
// account was created by an admin
13731378
func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form auth.MustChangePasswordForm) {
13741379
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
1375-
13761380
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
1377-
13781381
if ctx.HasError() {
13791382
ctx.HTML(200, tplMustChangePassword)
13801383
return
13811384
}
1382-
13831385
u := ctx.User
1384-
13851386
// Make sure only requests for users who are eligible to change their password via
13861387
// this method passes through
13871388
if !u.MustChangePassword {

routers/user/setting/account.go

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.gitea.io/gitea/modules/base"
1414
"code.gitea.io/gitea/modules/context"
1515
"code.gitea.io/gitea/modules/log"
16+
"code.gitea.io/gitea/modules/password"
1617
"code.gitea.io/gitea/modules/setting"
1718
"code.gitea.io/gitea/modules/timeutil"
1819
"code.gitea.io/gitea/services/mailer"
@@ -52,6 +53,8 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
5253
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
5354
} else if form.Password != form.Retype {
5455
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
56+
} else if !password.IsComplexEnough(form.Password) {
57+
ctx.Flash.Error(ctx.Tr("settings.password_complexity"))
5558
} else {
5659
var err error
5760
if ctx.User.Salt, err = models.GetUserSalt(); err != nil {

0 commit comments

Comments
 (0)