Handle missing or expired token in password reset action and LV gracefully (#3387)

This change addresses two problems:

* controller action crashing missing "token" param - it's handled gracefully
  now and will not pollute Sentry anymore with http://sentry.plausible.io/organizations/sentry/issues/4319
* LiveView receives email extracted from token on initial page load instead
  of reverifying token on every re-mount (which can happen when somebody
  leaves form open for an extended period of time; rare but happens and
  needlessly pollutes Sentry as well)
This commit is contained in:
Adrian Gruntkowski 2023-10-02 15:11:59 +02:00 committed by GitHub
parent e67850c11d
commit 16ce0f1ea8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 14 additions and 18 deletions

View File

@ -194,12 +194,12 @@ defmodule PlausibleWeb.AuthController do
end
end
def password_reset_form(conn, %{"token" => token}) do
case Auth.Token.verify_password_reset(token) do
{:ok, _} ->
def password_reset_form(conn, params) do
case Auth.Token.verify_password_reset(params["token"]) do
{:ok, %{email: email}} ->
render(conn, "password_reset_form.html",
connect_live_socket: true,
token: token,
email: email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)

View File

@ -11,11 +11,9 @@ defmodule PlausibleWeb.Live.ResetPasswordForm do
alias Plausible.Auth
alias Plausible.Repo
def mount(_params, %{"reset_token" => reset_token}, socket) do
def mount(_params, %{"email" => email}, socket) do
socket =
assign_new(socket, :user, fn ->
# by that point token should be already verified
{:ok, %{email: email}} = Auth.Token.verify_password_reset(reset_token)
Repo.get_by!(Auth.User, email: email)
end)
@ -24,7 +22,6 @@ defmodule PlausibleWeb.Live.ResetPasswordForm do
{:ok,
assign(socket,
form: to_form(changeset),
reset_token: reset_token,
password_strength: Auth.User.password_strength(changeset),
trigger_submit: false
)}
@ -60,7 +57,6 @@ defmodule PlausibleWeb.Live.ResetPasswordForm do
class="transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
/>
</div>
<input name="token" type="hidden" value={@reset_token} />
<button id="set" type="submit" class="button mt-4 w-full">
Set password
</button>

View File

@ -1,4 +1,4 @@
<%= live_render(@conn, PlausibleWeb.Live.ResetPasswordForm,
container: {:div, class: "contents"},
session: %{"reset_token" => @token}
session: %{"email" => @email}
) %>

View File

@ -409,16 +409,17 @@ defmodule PlausibleWeb.AuthControllerTest do
assert html_response(conn, 401) =~ "Your token is invalid"
end
test "without token - shows error page", %{conn: conn} do
conn = get(conn, "/password/reset", %{})
assert html_response(conn, 401) =~ "Your token is invalid"
end
end
describe "POST /password/reset" do
alias Plausible.Auth.Token
test "redirects the user to login and shows success message", %{conn: conn} do
user = insert(:user)
token = Token.sign_password_reset(user.email)
conn = post(conn, "/password/reset", %{token: token})
conn = post(conn, "/password/reset", %{})
assert location = "/login" = redirected_to(conn, 302)

View File

@ -18,9 +18,8 @@ defmodule PlausibleWeb.Live.ResetPasswordFormTest do
type_into_passowrd(lv, "very-secret-and-very-long-123")
html = lv |> element("form") |> render_submit()
assert [csrf_input, password_input, token_input | _] = find(html, "input")
assert [csrf_input, password_input | _] = find(html, "input")
assert String.length(text_of_attr(csrf_input, "value")) > 0
assert text_of_attr(token_input, "value") == token
assert text_of_attr(password_input, "value") == "very-secret-and-very-long-123"
assert %{password_hash: new_hash} = Repo.one(User)
assert new_hash != user.password_hash