diff --git a/apps/fz_http/assets/js/hooks.js b/apps/fz_http/assets/js/hooks.js index 578998b8c..d4d3a0018 100644 --- a/apps/fz_http/assets/js/hooks.js +++ b/apps/fz_http/assets/js/hooks.js @@ -1,5 +1,5 @@ import hljs from "highlight.js" -import {FormatTimestamp} from "./util.js" +import {FormatTimestamp,PasswordStrength} from "./util.js" import {renderQrCode} from "./qrcode.js" const highlightCode = function () { @@ -11,6 +11,46 @@ const formatTimestamp = function () { this.el.innerHTML = FormatTimestamp(t) } +const passwordStrength = function () { + const field = this.el + const fieldClasses = "password input " + const progress = document.getElementById(field.dataset.target) + const reset = function () { + field.className = fieldClasses + progress.className = "is-hidden" + progress.setAttribute("value", "0") + progress.innerHTML = "0%" + } + field.addEventListener("input", () => { + if (field.value === "") return reset() + const score = PasswordStrength(field.value) + switch (score) { + case 0: + case 1: + field.className = fieldClasses + "is-danger" + progress.className = "progress is-small is-danger" + progress.setAttribute("value", "33") + progress.innerHTML = "33%" + break + case 2: + case 3: + field.className = fieldClasses + "is-warning" + progress.className = "progress is-small is-warning" + progress.setAttribute("value", "67") + progress.innerHTML = "67%" + break + case 4: + field.className = fieldClasses + "is-success" + progress.className = "progress is-small is-success" + progress.setAttribute("value", "100") + progress.innerHTML = "100%" + break + default: + reset() + } + }) +} + const clipboardCopy = function () { let button = this.el let data = button.dataset.clipboard @@ -37,5 +77,9 @@ Hooks.FormatTimestamp = { mounted: formatTimestamp, updated: formatTimestamp } +Hooks.PasswordStrength = { + mounted: passwordStrength, + updated: passwordStrength +} export default Hooks diff --git a/apps/fz_http/assets/js/util.js b/apps/fz_http/assets/js/util.js index c9263a031..60983aa90 100644 --- a/apps/fz_http/assets/js/util.js +++ b/apps/fz_http/assets/js/util.js @@ -1,4 +1,5 @@ import moment from "moment" +import zxcvbn from "zxcvbn" const FormatTimestamp = function (timestamp) { if (timestamp) { @@ -8,4 +9,9 @@ const FormatTimestamp = function (timestamp) { } } -export { FormatTimestamp } +const PasswordStrength = function (password) { + const result = zxcvbn(password) + return result.score +} + +export { PasswordStrength, FormatTimestamp } diff --git a/apps/fz_http/assets/package-lock.json b/apps/fz_http/assets/package-lock.json index 87b4ba15d..03c984d41 100644 --- a/apps/fz_http/assets/package-lock.json +++ b/apps/fz_http/assets/package-lock.json @@ -43,7 +43,8 @@ "source-map": "^0.7.3", "url-loader": "^4.1.1", "webpack": "5.36.0", - "webpack-cli": "^4.9.2" + "webpack-cli": "^4.9.2", + "zxcvbn": "^4.4.2" }, "engines": { "node": ">= 14.0.0" @@ -8061,6 +8062,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=", + "dev": true } }, "dependencies": { @@ -13904,6 +13911,12 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=", + "dev": true } } } diff --git a/apps/fz_http/assets/package.json b/apps/fz_http/assets/package.json index 8454d69d6..56cd5cf5e 100644 --- a/apps/fz_http/assets/package.json +++ b/apps/fz_http/assets/package.json @@ -48,6 +48,7 @@ "source-map": "^0.7.3", "url-loader": "^4.1.1", "webpack": "5.36.0", - "webpack-cli": "^4.9.2" + "webpack-cli": "^4.9.2", + "zxcvbn": "^4.4.2" } } diff --git a/apps/fz_http/lib/fz_http/users/user.ex b/apps/fz_http/lib/fz_http/users/user.ex index 5e74ab5de..c930cc6c1 100644 --- a/apps/fz_http/lib/fz_http/users/user.ex +++ b/apps/fz_http/lib/fz_http/users/user.ex @@ -3,7 +3,7 @@ defmodule FzHttp.Users.User do Represents a User. """ - @min_password_length 8 + @min_password_length 12 @max_password_length 64 use Ecto.Schema @@ -173,4 +173,6 @@ defmodule FzHttp.Users.User do {:error, error_msg} -> changeset |> add_error(:current_password, error_msg) end end + + defp verify_current_password(changeset, _user), do: changeset end diff --git a/apps/fz_http/lib/fz_http_web/error_helpers.ex b/apps/fz_http/lib/fz_http_web/error_helpers.ex index 8cd1db5f6..b1bea5e8b 100644 --- a/apps/fz_http/lib/fz_http_web/error_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/error_helpers.ex @@ -29,6 +29,19 @@ defmodule FzHttpWeb.ErrorHelpers do end) end + @doc """ + Adds "is-danger" to input elements that have errors + """ + def input_error_class(form, field) do + case Keyword.get_values(form.errors, field) do + [] -> + "" + + _ -> + "is-danger" + end + end + @doc """ Translates an error message using gettext. """ diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex index 3168a8bad..64e031ad2 100644 --- a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex @@ -3,7 +3,7 @@
<%= label f, :name, class: "label" %>
- <%= text_input f, :name, class: "input" %> + <%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %>

<%= error_tag f, :name %> @@ -30,7 +30,8 @@

<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
- <%= text_input f, :allowed_ips, class: "input", disabled: @use_default_allowed_ips %> + <%= text_input f, :allowed_ips, class: "input #{input_error_class(f, :allowed_ips)}", + disabled: @use_default_allowed_ips %>

<%= error_tag f, :allowed_ips %> @@ -57,7 +58,8 @@

<%= label f, :dns, "DNS Servers", class: "label" %>
- <%= text_input f, :dns, class: "input", disabled: @use_default_dns %> + <%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}", + disabled: @use_default_dns %>

<%= error_tag f, :dns %> @@ -85,7 +87,8 @@ <%= label f, :endpoint, "Server Endpoint", class: "label" %>

The IP of the server this device should connect to.

- <%= text_input f, :endpoint, class: "input", disabled: @use_default_endpoint %> + <%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}", + disabled: @use_default_endpoint %>

<%= error_tag f, :endpoint %> @@ -113,7 +116,7 @@ <%= label f, :mtu, "Interface MTU", class: "label" %>

The WireGuard interface MTU for this Device.

- <%= text_input f, :mtu, class: "input", disabled: @use_default_mtu %> + <%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_default_mtu %>

<%= error_tag f, :mtu %> @@ -146,7 +149,8 @@ unless you're experiencing NAT or firewall traversal problems.

- <%= text_input f, :persistent_keepalive, class: "input", disabled: @use_default_persistent_keepalive %> + <%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}", + disabled: @use_default_persistent_keepalive %>

<%= error_tag f, :persistent_keepalive %> @@ -157,7 +161,7 @@ <%= label f, :ipv4, "IPv4 Address", class: "label" %>

- <%= text_input f, :ipv4, class: "input" %> + <%= text_input f, :ipv4, class: "input #{input_error_class(f, :ipv4)}" %>

<%= error_tag f, :ipv4 %> @@ -168,7 +172,7 @@ <%= label f, :ipv6, "IPv6 Address", class: "label" %>

- <%= text_input f, :ipv6, class: "input" %> + <%= text_input f, :ipv6, class: "input #{input_error_class(f, :ipv6)}" %>

<%= error_tag f, :ipv6 %> diff --git a/apps/fz_http/lib/fz_http_web/live/rule_live/rule_list_component.html.heex b/apps/fz_http/lib/fz_http_web/live/rule_live/rule_list_component.html.heex index 44e20768a..d3bb5ee71 100644 --- a/apps/fz_http/lib/fz_http_web/live/rule_live/rule_list_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/rule_live/rule_list_component.html.heex @@ -12,7 +12,7 @@

<%= text_input f, :destination, - class: "input", + class: "input #{input_error_class(f, :destination)}", placeholder: "IPv4/6 CIDR range or address" %>
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex index 74f6a902d..58f15b8db 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex @@ -8,32 +8,24 @@ <%= label f, :email, class: "label" %>
- <%= text_input f, :email, class: "input" %> + <%= text_input f, :email, class: "input #{input_error_class(f, :email)}" %>

<%= error_tag f, :email %>

-
- <%= label f, :password, "New password", class: "label" %> -
- <%= password_input f, :password, class: "input password" %> -
-

- <%= error_tag f, :password %> -

-
+ <%= render FzHttpWeb.SharedView, + "password_field.html", + context: f, + field: :password, + label: "Password" %> -
- <%= label f, :password_confirmation, "New password confirmation", class: "label" %> -
- <%= password_input f, :password_confirmation, class: "input password" %> -
-

- <%= error_tag f, :password_confirmation %> -

-
+ <%= render FzHttpWeb.SharedView, + "password_field.html", + context: f, + field: :password_confirmation, + label: "Password Confirmation" %>
diff --git a/apps/fz_http/lib/fz_http_web/live/user_live/form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/user_live/form_component.html.heex index 02fb6241f..0b3391f91 100644 --- a/apps/fz_http/lib/fz_http_web/live/user_live/form_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/user_live/form_component.html.heex @@ -10,34 +10,24 @@ <%= label f, :email, class: "label" %>
- <%= text_input f, :email, class: "input" %> + <%= text_input f, :email, class: "input #{input_error_class(f, :email)}" %>

<%= error_tag f, :email %>

-
- <%= label f, :password, "New Password", class: "label" %> + <%= render FzHttpWeb.SharedView, + "password_field.html", + context: f, + field: :password, + label: "New Password" %> -
- <%= password_input f, :password, class: "input password" %> -
-

- <%= error_tag f, :password %> -

-
- -
- <%= label f, :password_confirmation, "New Password Confirmation", class: "label" %> - -
- <%= password_input f, :password_confirmation, class: "input password" %> -
-

- <%= error_tag f, :password_confirmation %> -

-
+ <%= render FzHttpWeb.SharedView, + "password_field.html", + context: f, + field: :password_confirmation, + label: "New Password Confirmation" %>
diff --git a/apps/fz_http/lib/fz_http_web/templates/session/new.html.eex b/apps/fz_http/lib/fz_http_web/templates/session/new.html.heex similarity index 80% rename from apps/fz_http/lib/fz_http_web/templates/session/new.html.eex rename to apps/fz_http/lib/fz_http_web/templates/session/new.html.heex index cb8792510..ab3a9183f 100644 --- a/apps/fz_http/lib/fz_http_web/templates/session/new.html.eex +++ b/apps/fz_http/lib/fz_http_web/templates/session/new.html.heex @@ -1,4 +1,3 @@ -

Sign In


@@ -13,7 +12,7 @@
<%= label(:session, :email, class: "label") %>
- <%= text_input(f, :email, class: "input") %> + <%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %>

<%= error_tag f, :email %> @@ -23,7 +22,7 @@

<%= label(:session, :password, class: "label") %>
- <%= password_input(f, :password, class: "input") %> + <%= password_input(f, :password, class: "input #{input_error_class(f, :password)}") %>

<%= error_tag f, :password %> diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/password_field.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/password_field.html.heex new file mode 100644 index 000000000..43df49c32 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/shared/password_field.html.heex @@ -0,0 +1,21 @@ +

+ <%= label @context, @field, @label, class: "label" %> + +
+ <%= password_input @context, + @field, + class: "input password", + id: "#{@field}-field", + data_target: "#{@field}-progress", + phx_hook: "PasswordStrength" %> +
+

+ <%= error_tag @context, @field %> +

+ + +
diff --git a/apps/fz_http/test/fz_http/release_test.exs b/apps/fz_http/test/fz_http/release_test.exs index 860d27ce9..4f33dc1a6 100644 --- a/apps/fz_http/test/fz_http/release_test.exs +++ b/apps/fz_http/test/fz_http/release_test.exs @@ -31,7 +31,7 @@ defmodule FzHttp.ReleaseTest do test "reset admin password when user exists" do {:ok, first_user} = Release.create_admin_user() - {:ok, new_first_user} = Release.change_password(first_user.email, "newpassword") + {:ok, new_first_user} = Release.change_password(first_user.email, "newpassword1234") {:ok, second_user} = Release.create_admin_user() assert second_user.password_hash != new_first_user.password_hash diff --git a/apps/fz_http/test/fz_http/sessions_test.exs b/apps/fz_http/test/fz_http/sessions_test.exs index 70c9426da..6b2aee38f 100644 --- a/apps/fz_http/test/fz_http/sessions_test.exs +++ b/apps/fz_http/test/fz_http/sessions_test.exs @@ -36,7 +36,7 @@ defmodule FzHttp.SessionsTest do describe "create_session/2" do setup [:create_user] - @password_params %{password: "testtest"} + @password_params %{password: "password1234"} @invalid_params %{password: "invalid"} test "creates session (updates existing record)", %{user: user} do diff --git a/apps/fz_http/test/fz_http/users_test.exs b/apps/fz_http/test/fz_http/users_test.exs index 0bfb0f330..0048556c5 100644 --- a/apps/fz_http/test/fz_http/users_test.exs +++ b/apps/fz_http/test/fz_http/users_test.exs @@ -61,23 +61,23 @@ defmodule FzHttp.UsersTest do describe "create_user/1" do @valid_attrs_map %{ email: "valid@test", - password: "password", - password_confirmation: "password" + password: "password1234", + password_confirmation: "password1234" } @valid_attrs_list [ email: "valid@test", - password: "password", - password_confirmation: "password" + password: "password1234", + password_confirmation: "password1234" ] @invalid_attrs_map %{ email: "invalid_email", - password: "password", - password_confirmation: "password" + password: "password1234", + password_confirmation: "password1234" } @invalid_attrs_list [ email: "valid@test", - password: "password", - password_confirmation: "different_password" + password: "password1234", + password_confirmation: "different_password1234" ] @too_short_password [ email: "valid@test", @@ -95,7 +95,7 @@ defmodule FzHttp.UsersTest do assert changeset.errors[:password] == { "should be at least %{count} character(s)", - [count: 8, validation: :length, kind: :min, type: :string] + [count: 12, validation: :length, kind: :min, type: :string] } end @@ -140,7 +140,7 @@ defmodule FzHttp.UsersTest do @change_password_valid_params %{ "password" => "new_password", "password_confirmation" => "new_password", - "current_password" => "testtest" + "current_password" => "password1234" } @change_password_invalid_params %{ "password" => "new_password", diff --git a/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs index 125ea5647..290863fbb 100644 --- a/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs +++ b/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs @@ -54,7 +54,7 @@ defmodule FzHttpWeb.SessionControllerTest do params = %{ "session" => %{ "email" => user.email, - "password" => "testtest" + "password" => "password1234" } } diff --git a/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs index 44216d543..ae60045d3 100644 --- a/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs +++ b/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs @@ -261,7 +261,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do |> render_change(@default_allowed_ips_change) assert test_view =~ """ - \ + \ """ end @@ -275,7 +275,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do |> render_change(@default_dns_change) assert test_view =~ """ - \ + \ """ end @@ -289,7 +289,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do |> render_change(@default_endpoint_change) assert test_view =~ """ - \ + \ """ end @@ -303,7 +303,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do |> render_change(@default_mtu_change) assert test_view =~ """ - \ + \ """ end @@ -317,7 +317,7 @@ defmodule FzHttpWeb.DeviceLive.ShowTest do |> render_change(@default_persistent_keepalive_change) assert test_view =~ """ - \ + \ """ end end diff --git a/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs index 1b0724866..6822f44e5 100644 --- a/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs +++ b/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs @@ -98,7 +98,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do |> render_submit(@invalid_user_attrs) assert new_view =~ "has invalid format" - assert new_view =~ "should be at least 8 character(s)" + assert new_view =~ "should be at least 12 character(s)" end end diff --git a/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs index f93752fca..e2545e6c3 100644 --- a/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs +++ b/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs @@ -139,7 +139,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do |> render_submit(@invalid_attrs) assert new_view =~ "has invalid format" - assert new_view =~ "should be at least 8 character(s)" + assert new_view =~ "should be at least 12 character(s)" end end diff --git a/apps/fz_http/test/support/fixtures/sessions_fixtures.ex b/apps/fz_http/test/support/fixtures/sessions_fixtures.ex index 147422091..1f5086a46 100644 --- a/apps/fz_http/test/support/fixtures/sessions_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/sessions_fixtures.ex @@ -8,7 +8,7 @@ defmodule FzHttp.SessionsFixtures do def session(_attrs \\ %{}) do email = UsersFixtures.user().email record = Sessions.get_session!(email: email) - create_params = %{email: email, password: "testtest"} + create_params = %{email: email, password: "password1234"} {:ok, session} = Sessions.create_session(record, create_params) session end diff --git a/apps/fz_http/test/support/fixtures/users_fixtures.ex b/apps/fz_http/test/support/fixtures/users_fixtures.ex index 04b1769d6..f1f0bbaaa 100644 --- a/apps/fz_http/test/support/fixtures/users_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/users_fixtures.ex @@ -15,7 +15,7 @@ defmodule FzHttp.UsersFixtures do case Repo.get_by(User, email: email) do nil -> {:ok, user} = - %{email: email, password: "testtest", password_confirmation: "testtest"} + %{email: email, password: "password1234", password_confirmation: "password1234"} |> Map.merge(attrs) |> Users.create_admin_user() diff --git a/config/config.exs b/config/config.exs index c5c2b2de1..0290f3720 100644 --- a/config/config.exs +++ b/config/config.exs @@ -65,7 +65,7 @@ config :fz_http, cookie_signing_salt: "Z9eq8iof", ecto_repos: [FzHttp.Repo], admin_email: "firezone@localhost", - default_admin_password: "firezone", + default_admin_password: "firezone1234", events_module: FzHttpWeb.Events, server_process_opts: [name: {:global, :fz_http_server}] diff --git a/omnibus/cookbooks/firezone/libraries/config.rb b/omnibus/cookbooks/firezone/libraries/config.rb index f922f06f5..32d57ffd9 100644 --- a/omnibus/cookbooks/firezone/libraries/config.rb +++ b/omnibus/cookbooks/firezone/libraries/config.rb @@ -115,7 +115,7 @@ class Firezone 'database_encryption_key' => node['firezone'] && node['firezone']['database_encryption_key'] || \ SecureRandom.base64(32), 'default_admin_password' => node['firezone'] && node['firezone']['default_admin_password'] || \ - SecureRandom.base64(8) + SecureRandom.base64(12) } end # rubocop:enable Metrics/PerceivedComplexity