feat: add minimum password entropy requirements (#6090)

* feat: add minimum password entropy requirements

* Fix all the tests

* Fix E2E tests
This commit is contained in:
Kyle Carberry 2023-02-08 14:10:08 -06:00 committed by GitHub
parent fe725f76bb
commit 2ed0eafd75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 122 additions and 63 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/userpassword"
"github.com/coder/coder/codersdk"
)
@ -152,16 +153,19 @@ func login() *cobra.Command {
for !matching {
password, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: func(s string) error {
return userpassword.Validate(s)
},
})
if err != nil {
return xerrors.Errorf("specify password prompt: %w", err)
}
confirm, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
if err != nil {
return xerrors.Errorf("confirm password prompt: %w", err)

View File

@ -54,8 +54,8 @@ func TestLogin(t *testing.T) {
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "password",
"password", "password", // Confirm.
"password", "SomeSecurePassword!",
"password", "SomeSecurePassword!", // Confirm.
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
@ -89,8 +89,8 @@ func TestLogin(t *testing.T) {
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "password",
"password", "password", // Confirm.
"password", "SomeSecurePassword!",
"password", "SomeSecurePassword!", // Confirm.
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
@ -107,7 +107,7 @@ func TestLogin(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", client.URL.String(), "--first-user-username", "testuser", "--first-user-email", "user@coder.com", "--first-user-password", "password", "--first-user-trial")
root, _ := clitest.New(t, "login", client.URL.String(), "--first-user-username", "testuser", "--first-user-email", "user@coder.com", "--first-user-password", "SomeSecurePassword!", "--first-user-trial")
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
@ -143,8 +143,8 @@ func TestLogin(t *testing.T) {
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "mypass",
"password", "wrongpass", // Confirm.
"password", "MyFirstSecurePassword!",
"password", "MyNonMatchingSecurePassword!", // Confirm.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
@ -157,9 +157,9 @@ func TestLogin(t *testing.T) {
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("Enter a " + cliui.Styles.Field.Render("password"))
pty.WriteLine("pass")
pty.WriteLine("SomeSecurePassword!")
pty.ExpectMatch("Confirm")
pty.WriteLine("pass")
pty.WriteLine("SomeSecurePassword!")
pty.ExpectMatch("trial")
pty.WriteLine("yes")
pty.ExpectMatch("Welcome to Coder")

View File

@ -50,9 +50,11 @@ func resetPassword() *cobra.Command {
}
password, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Enter new " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
Text: "Enter new " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: func(s string) error {
return userpassword.Validate(s)
},
})
if err != nil {
return xerrors.Errorf("password prompt: %w", err)

View File

@ -28,8 +28,8 @@ func TestResetPassword(t *testing.T) {
const email = "some@one.com"
const username = "example"
const oldPassword = "password"
const newPassword = "password2"
const oldPassword = "MyOldPassword!"
const newPassword = "MyNewPassword!"
// start postgres and coder server processes

View File

@ -428,7 +428,7 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui
var FirstUserParams = codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Password: "SomeSecurePassword!",
}
// CreateFirstUser creates a user with preset credentials and authenticates
@ -455,7 +455,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
req := codersdk.CreateUserRequest{
Email: namesgenerator.GetRandomName(10) + "@coder.com",
Username: randomUsername(),
Password: "testpass",
Password: "SomeSecurePassword!",
OrganizationID: organizationID,
}

View File

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
@ -125,15 +126,14 @@ func hashWithSaltAndIter(password string, salt []byte, iter int) string {
// Validate checks that the plain text password meets the minimum password requirements.
// It returns properly formatted errors for detailed form validation on the client.
func Validate(password string) error {
const (
minLength = 8
maxLength = 64
)
if len(password) < minLength {
return xerrors.Errorf("Password must be at least %d characters.", minLength)
// Ensure passwords are secure enough!
// See: https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use
err := passwordvalidator.Validate(password, 52)
if err != nil {
return err
}
if len(password) > maxLength {
return xerrors.Errorf("Password must be no more than %d characters.", maxLength)
if len(password) > 64 {
return xerrors.Errorf("password must be no more than %d characters", 64)
}
return nil
}

View File

@ -105,6 +105,18 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
}
err = userpassword.Validate(createUser.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Password not strong enough!",
Validations: []codersdk.ValidationError{{
Field: "password",
Detail: err.Error(),
}},
})
return
}
user, organizationID, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
@ -316,6 +328,18 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
err = userpassword.Validate(req.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Password not strong enough!",
Validations: []codersdk.ValidationError{{
Field: "password",
Detail: err.Error(),
}},
})
return
}
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequest: req,
LoginType: database.LoginTypePassword,

View File

@ -49,7 +49,7 @@ func TestFirstUser(t *testing.T) {
_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: "some@email.com",
Username: "exampleuser",
Password: "password",
Password: "SomeSecurePassword!",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@ -78,7 +78,7 @@ func TestFirstUser(t *testing.T) {
req := codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Password: "SomeSecurePassword!",
Trial: true,
}
_, err := client.CreateFirstUser(ctx, req)
@ -123,7 +123,7 @@ func TestPostLogin(t *testing.T) {
req := codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Password: "SomeSecurePassword!",
}
_, err := client.CreateFirstUser(ctx, req)
require.NoError(t, err)
@ -172,7 +172,7 @@ func TestPostLogin(t *testing.T) {
// Test a new session
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: memberUser.Email,
Password: "testpass",
Password: "SomeSecurePassword!",
})
numLogs++ // add an audit log for login
require.ErrorAs(t, err, &apiErr)
@ -198,7 +198,7 @@ func TestPostLogin(t *testing.T) {
defer cancel()
// With a user account.
const password = "testpass"
const password = "SomeSecurePassword!"
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test+user-@coder.com",
Username: "user",
@ -245,7 +245,7 @@ func TestPostLogin(t *testing.T) {
req := codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Password: "SomeSecurePassword!",
}
_, err := client.CreateFirstUser(ctx, req)
require.NoError(t, err)
@ -309,7 +309,7 @@ func TestDeleteUser(t *testing.T) {
another, err = api.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: another.Email,
Username: another.Username,
Password: "testing",
Password: "SomeSecurePassword!",
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
@ -421,7 +421,7 @@ func TestPostUsers(t *testing.T) {
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: me.Email,
Username: me.Username,
Password: "password",
Password: "MySecurePassword!",
OrganizationID: uuid.New(),
})
var apiErr *codersdk.Error
@ -441,7 +441,7 @@ func TestPostUsers(t *testing.T) {
OrganizationID: uuid.New(),
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
Password: "SomeSecurePassword!",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@ -466,7 +466,7 @@ func TestPostUsers(t *testing.T) {
_, err = notInOrg.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "some@domain.com",
Username: "anotheruser",
Password: "testing",
Password: "SomeSecurePassword!",
OrganizationID: org.ID,
})
var apiErr *codersdk.Error
@ -491,7 +491,7 @@ func TestPostUsers(t *testing.T) {
OrganizationID: user.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
@ -561,7 +561,7 @@ func TestUpdateUserProfile(t *testing.T) {
existentUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Password: "password",
Password: "SomeSecurePassword!",
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
@ -627,18 +627,18 @@ func TestUpdateUserPassword(t *testing.T) {
member, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "password",
Password: "SomeStrongPassword!",
OrganizationID: admin.OrganizationID,
})
require.NoError(t, err, "create member")
err = client.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
Password: "SomeNewStrongPassword!",
})
require.NoError(t, err, "admin should be able to update member password")
// Check if the member can login using the new password
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: "coder@coder.com",
Password: "newpassword",
Password: "SomeNewStrongPassword!",
})
require.NoError(t, err, "member should login successfully with the new password")
})
@ -659,8 +659,8 @@ func TestUpdateUserPassword(t *testing.T) {
defer cancel()
err := member.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
OldPassword: "testpass",
Password: "newpassword",
OldPassword: "SomeSecurePassword!",
Password: "MyNewSecurePassword!",
})
numLogs++ // add an audit log for user update
@ -696,7 +696,7 @@ func TestUpdateUserPassword(t *testing.T) {
defer cancel()
err := client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
Password: "MySecurePassword!",
})
numLogs++ // add an audit log for user update
@ -720,7 +720,7 @@ func TestUpdateUserPassword(t *testing.T) {
require.NoError(t, err)
err = client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
Password: "MyNewSecurePassword!",
})
require.NoError(t, err)
@ -733,7 +733,7 @@ func TestUpdateUserPassword(t *testing.T) {
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: "newpassword",
Password: "MyNewSecurePassword!",
})
require.NoError(t, err)
@ -1264,7 +1264,7 @@ func TestGetUsers(t *testing.T) {
client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
})
// No params is all users
@ -1290,7 +1290,7 @@ func TestGetUsers(t *testing.T) {
alice, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
@ -1298,7 +1298,7 @@ func TestGetUsers(t *testing.T) {
bruno, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "bruno@email.com",
Username: "bruno",
Password: "password",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
@ -1329,7 +1329,7 @@ func TestGetUsersPagination(t *testing.T) {
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
@ -1410,13 +1410,13 @@ func TestWorkspacesByUser(t *testing.T) {
newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test@coder.com",
Username: "someone",
Password: "password",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
auth, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: newUser.Email,
Password: "password",
Password: "MySecurePassword!",
})
require.NoError(t, err)
@ -1463,7 +1463,7 @@ func TestSuspendedPagination(t *testing.T) {
user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: "password",
Password: "MySecurePassword!",
OrganizationID: orgID,
})
require.NoError(t, err)
@ -1526,7 +1526,7 @@ func TestPaginatedUsers(t *testing.T) {
newUser, err := client.CreateUser(egCtx, codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: "password",
Password: "MySecurePassword!",
OrganizationID: orgID,
})
if err != nil {

View File

@ -1138,7 +1138,7 @@ func TestAppSharing(t *testing.T) {
setup := func(t *testing.T, allowPathAppSharing, allowSiteOwnerAccess bool) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, ownerClient *codersdk.Client, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) {
//nolint:gosec
const password = "password"
const password = "SomeSecurePassword!"
var port uint16
ownerClient, _, _, port = setupProxyTest(t, &setupProxyTestOpts{

1
go.mod
View File

@ -196,6 +196,7 @@ require (
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.1 // indirect
github.com/wagslane/go-password-validator v0.3.0 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
)

2
go.sum
View File

@ -1878,6 +1878,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=

View File

@ -3,5 +3,5 @@ export const defaultPort = 3000
// Credentials for the first user
export const username = "admin"
export const password = "password"
export const password = "SomeSecurePassword!"
export const email = "admin@coder.com"

View File

@ -644,8 +644,22 @@ export const putWorkspaceExtension = async (
}
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
const response = await axios.get("/api/v2/entitlements")
return response.data
try {
const response = await axios.get("/api/v2/entitlements")
return response.data
} catch (ex) {
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
return {
errors: [],
experimental: false,
features: withDefaultFeatures({}),
has_license: false,
trial: false,
warnings: [],
}
}
throw ex
}
}
export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
@ -777,8 +791,20 @@ export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
}
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
const response = await axios.get(`/api/v2/appearance`)
return response.data
try {
const response = await axios.get(`/api/v2/appearance`)
return response.data || {}
} catch (ex) {
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
return {
logo_url: "",
service_banner: {
enabled: false,
},
}
}
throw ex
}
}
export const updateAppearance = async (