diff --git a/cli/login.go b/cli/login.go index 03332a0f8ff8e..bda389c3fe771 100644 --- a/cli/login.go +++ b/cli/login.go @@ -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) diff --git a/cli/login_test.go b/cli/login_test.go index bf4507b61beca..14f9208360002 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -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") diff --git a/cli/resetpassword.go b/cli/resetpassword.go index 048e473e55951..72b6d6570514e 100644 --- a/cli/resetpassword.go +++ b/cli/resetpassword.go @@ -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) diff --git a/cli/resetpassword_test.go b/cli/resetpassword_test.go index 66934715845a2..8bcf4777dfe92 100644 --- a/cli/resetpassword_test.go +++ b/cli/resetpassword_test.go @@ -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 diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 93a4dbeba966f..9324675ccdd81 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -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, } diff --git a/coderd/userpassword/userpassword.go b/coderd/userpassword/userpassword.go index ac4d9f6735eaf..6f0da0e9aac64 100644 --- a/coderd/userpassword/userpassword.go +++ b/coderd/userpassword/userpassword.go @@ -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 } diff --git a/coderd/users.go b/coderd/users.go index adcfecdccf958..9f49a74aec64b 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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, diff --git a/coderd/users_test.go b/coderd/users_test.go index ec24f6e667281..283a50a25e9d8 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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 { diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index 9a30c79da19dc..40f08424b881d 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -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{ diff --git a/go.mod b/go.mod index d068dbfd66a18..07bf115129772 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 4750e7ff1ae84..6afa60e403d25 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index 5227e6d50175e..1e56c3d8b3b28 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -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" diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8532101f6f49e..2b13f2ded771d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -644,8 +644,22 @@ export const putWorkspaceExtension = async ( } export const getEntitlements = async (): Promise => { - 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 => { @@ -777,8 +791,20 @@ export const getFile = async (fileId: string): Promise => { } export const getAppearance = async (): Promise => { - 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 (