Skip to content

Commit 9d8aaea

Browse files
committed
feat: add minimum password entropy requirements
1 parent bde4ffe commit 9d8aaea

File tree

6 files changed

+75
-16
lines changed

6 files changed

+75
-16
lines changed

cli/login.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/coder/coder/cli/cliflag"
2121
"github.com/coder/coder/cli/cliui"
22+
"github.com/coder/coder/coderd/userpassword"
2223
"github.com/coder/coder/codersdk"
2324
)
2425

@@ -152,16 +153,24 @@ func login() *cobra.Command {
152153

153154
for !matching {
154155
password, err = cliui.Prompt(cmd, cliui.PromptOptions{
155-
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
156-
Secret: true,
157-
Validate: cliui.ValidateNotEmpty,
156+
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
157+
Secret: true,
158+
Validate: func(s string) error {
159+
return userpassword.Validate(s)
160+
},
158161
})
159162
if err != nil {
160163
return xerrors.Errorf("specify password prompt: %w", err)
161164
}
162165
confirm, err := cliui.Prompt(cmd, cliui.PromptOptions{
163166
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
164167
Secret: true,
168+
Validate: func(s string) error {
169+
if s != password {
170+
return xerrors.New("Passwords do not match")
171+
}
172+
return nil
173+
},
165174
})
166175
if err != nil {
167176
return xerrors.Errorf("confirm password prompt: %w", err)

coderd/userpassword/userpassword.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strconv"
1111
"strings"
1212

13+
passwordvalidator "github.com/wagslane/go-password-validator"
1314
"golang.org/x/crypto/pbkdf2"
1415
"golang.org/x/exp/slices"
1516
"golang.org/x/xerrors"
@@ -125,15 +126,11 @@ func hashWithSaltAndIter(password string, salt []byte, iter int) string {
125126
// Validate checks that the plain text password meets the minimum password requirements.
126127
// It returns properly formatted errors for detailed form validation on the client.
127128
func Validate(password string) error {
128-
const (
129-
minLength = 8
130-
maxLength = 64
131-
)
132-
if len(password) < minLength {
133-
return xerrors.Errorf("Password must be at least %d characters.", minLength)
134-
}
135-
if len(password) > maxLength {
136-
return xerrors.Errorf("Password must be no more than %d characters.", maxLength)
129+
// Ensure passwords are secure enough!
130+
// See: https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use
131+
err := passwordvalidator.Validate(password, 52)
132+
if err != nil {
133+
return err
137134
}
138135
return nil
139136
}

coderd/users.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
105105
}
106106
}
107107

108+
err = userpassword.Validate(createUser.Password)
109+
if err != nil {
110+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
111+
Message: "Password not strong enough!",
112+
Validations: []codersdk.ValidationError{{
113+
Field: "password",
114+
Detail: err.Error(),
115+
}},
116+
})
117+
return
118+
}
119+
108120
user, organizationID, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
109121
CreateUserRequest: codersdk.CreateUserRequest{
110122
Email: createUser.Email,
@@ -316,6 +328,18 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
316328
return
317329
}
318330

331+
err = userpassword.Validate(req.Password)
332+
if err != nil {
333+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
334+
Message: "Password not strong enough!",
335+
Validations: []codersdk.ValidationError{{
336+
Field: "password",
337+
Detail: err.Error(),
338+
}},
339+
})
340+
return
341+
}
342+
319343
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
320344
CreateUserRequest: req,
321345
LoginType: database.LoginTypePassword,

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ require (
190190
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
191191
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
192192
github.com/vmihailenco/tagparser v0.1.1 // indirect
193+
github.com/wagslane/go-password-validator v0.3.0 // indirect
193194
)
194195

195196
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC
18631863
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
18641864
github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
18651865
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
1866+
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
1867+
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
18661868
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
18671869
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
18681870
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=

site/src/api/api.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,22 @@ export const putWorkspaceExtension = async (
644644
}
645645

646646
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
647-
const response = await axios.get("/api/v2/entitlements")
648-
return response.data
647+
try {
648+
const response = await axios.get("/api/v2/entitlements")
649+
return response.data
650+
} catch (ex) {
651+
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
652+
return {
653+
errors: [],
654+
experimental: false,
655+
features: withDefaultFeatures({}),
656+
has_license: false,
657+
trial: false,
658+
warnings: [],
659+
}
660+
}
661+
throw ex
662+
}
649663
}
650664

651665
export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
@@ -777,8 +791,20 @@ export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
777791
}
778792

779793
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
780-
const response = await axios.get(`/api/v2/appearance`)
781-
return response.data
794+
try {
795+
const response = await axios.get(`/api/v2/appearance`)
796+
return response.data || {}
797+
} catch (ex) {
798+
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
799+
return {
800+
logo_url: "",
801+
service_banner: {
802+
enabled: false,
803+
},
804+
}
805+
}
806+
throw ex
807+
}
782808
}
783809

784810
export const updateAppearance = async (

0 commit comments

Comments
 (0)