diff --git a/.vscode/thunder-tests/thunderclient.db b/.vscode/thunder-tests/thunderclient.db index fccb0fd6..c5a77169 100644 --- a/.vscode/thunder-tests/thunderclient.db +++ b/.vscode/thunder-tests/thunderclient.db @@ -9,7 +9,9 @@ {"containerId":"62ce82a7-3adf-421e-ab6f-a82a6c3d3b30","sortNum":140000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/apps/:app_id/builds","url":"{{endpoint}}/api/apps/{{app_id}}/builds","method":"GET","modified":"2022-02-07T14:40:50.556Z","created":"2022-02-07T14:40:42.762Z","_id":"6e87ed47-8c53-49ad-8808-0b73a29ed30d","params":[],"tests":[]} {"containerId":"92e1989e-b7e4-4274-847e-4a81bab928fe","sortNum":120000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/apps/:app_id/environments","url":"{{endpoint}}/api/apps/{{app_id}}/environments","method":"POST","modified":"2022-02-07T14:38:06.932Z","created":"2022-02-07T14:37:48.537Z","_id":"80a611dd-15be-4498-8606-6d4df450a71d","params":[],"body":{"type":"json","raw":"{\n \"name\": \"test\", \n \"is_ephemeral\": false\n}","form":[]},"tests":[]} {"containerId":"880bc176-0b50-44c1-b255-0b804958e79a","sortNum":200000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/runner/builds/:build_id","url":"{{endpoint}}/runner/builds/{{build_id}}?secret={{runner_secret}}","method":"PUT","modified":"2022-02-07T14:52:24.876Z","created":"2022-02-07T14:46:14.276Z","_id":"8fd40007-6b67-4f63-993a-48af573b1c83","params":[{"name":"secret","value":"{{runner_secret}}","isPath":false}],"body":{"type":"json","raw":"{\n \"status\": \"success\"\n}","form":[]},"tests":[]} +{"containerId":"4bf14235-9c4a-4826-98f4-26eda1430256","sortNum":230000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/password/lost","url":"{{endpoint}}/auth/password/lost","method":"POST","modified":"2022-02-09T08:55:19.524Z","created":"2022-02-09T08:55:06.117Z","_id":"9b950d6f-61fc-436e-8525-aa2d5013478d","params":[],"body":{"type":"json","raw":"{\n \"email\": \"{{email}}\"\n}","form":[]},"tests":[]} {"containerId":"2cd2ddaa-4e51-4e24-98a3-0aa25fa60e9a","sortNum":10000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/apps","url":"{{endpoint}}/api/apps","method":"GET","modified":"2022-02-07T14:34:47.437Z","created":"2022-02-07T13:33:12.352Z","_id":"9e90a63f-52ab-4882-8228-99728cc789fa","params":[],"tests":[]} -{"containerId":"a94213ca-8f76-4321-98c8-c7963d59efb9","sortNum":80000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/verify/dev","url":"{{endpoint}}/api/verify/dev","method":"PUT","modified":"2022-02-08T10:39:10.877Z","created":"2022-02-07T14:08:57.271Z","_id":"b10f0e02-4ea0-4d37-ac20-70fb71f40f36","params":[],"body":{"type":"json","raw":"{\n \"code\": \"{{dev_code}}\"\n}","form":[]},"tests":[{"type":"set-env-var","custom":"json.data.access_token","action":"setto","value":"{{access_token}}"}]} +{"containerId":"4bf14235-9c4a-4826-98f4-26eda1430256","sortNum":240000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/password/lost","url":"{{endpoint}}/auth/password/lost","method":"PUT","modified":"2022-02-09T09:00:59.866Z","created":"2022-02-09T08:55:37.432Z","_id":"a9f67f98-9262-4526-9713-e83174da9208","params":[],"body":{"type":"json","raw":"{\n \"email\": \"{{email}}\",\n \"code\": \"{{password_lost_code}}\",\n \"password\": \"{{new_password}}\",\n \"password_confirmation\": \"{{new_password}}\"\n}","form":[]},"tests":[]} +{"containerId":"a94213ca-8f76-4321-98c8-c7963d59efb9","sortNum":80000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/verify/dev","url":"{{endpoint}}/api/verify/dev","method":"PUT","modified":"2022-02-07T14:09:21.910Z","created":"2022-02-07T14:08:57.271Z","_id":"b10f0e02-4ea0-4d37-ac20-70fb71f40f36","params":[],"body":{"type":"json","raw":"{\n \"code\": \"caeee9e4-2bc7-4c4f-b3fa-a64a2a53d97d\"\n}","form":[]},"tests":[]} {"containerId":"92e1989e-b7e4-4274-847e-4a81bab928fe","sortNum":110000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/app/:app_id/environments","url":"{{endpoint}}/api/apps/{{app_id}}/environments","method":"GET","modified":"2022-02-07T14:37:14.546Z","created":"2022-02-07T14:35:31.436Z","_id":"bc5eb2e8-2365-458e-beaf-70a9f30807cd","params":[],"tests":[]} {"containerId":"997859ea-5139-4d88-9114-97a8c7e102f3","sortNum":190000,"headers":[{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"Thunder Client (https://www.thunderclient.com)"}],"colId":"786cd85e-2a59-44b6-a835-45cd23e28b73","name":"/me/apps","url":"{{endpoint}}/api/me/apps","method":"GET","modified":"2022-02-07T14:44:55.745Z","created":"2022-02-07T14:44:53.103Z","_id":"cf80af68-34b9-4c7c-83df-19efe66dcd08","params":[],"tests":[]} diff --git a/apps/lenra/lib/lenra/services/password_services.ex b/apps/lenra/lib/lenra/services/password_services.ex index f258b2d7..bf09152b 100644 --- a/apps/lenra/lib/lenra/services/password_services.ex +++ b/apps/lenra/lib/lenra/services/password_services.ex @@ -30,10 +30,14 @@ defmodule Lenra.PasswordServices do def update_lost_password( %User{} = user, + %PasswordCode{} = password_code, params ) do with :ok <- check_old_password(user, params) do - Repo.insert(Password.new(user, params)) + Ecto.Multi.new() + |> Ecto.Multi.insert(:new_password, Password.new(user, params)) + |> Ecto.Multi.delete(:delete_password_code, password_code) + |> Repo.transaction() end end @@ -74,25 +78,19 @@ defmodule Lenra.PasswordServices do end def check_password_code_valid(%User{} = user, code) do - password_code = - Repo.all( - from(u in User, - join: p in assoc(u, :password_code), - where: p.code == ^code, - select: p - ) - ) + user = Repo.preload(user, [:password_code]) - with true <- not is_nil(List.first(password_code)), - true <- date_difference(password_code) do - {:ok, user} + with true <- not is_nil(user.password_code), + true <- user.password_code.code == code, + true <- date_difference(user.password_code) do + {:ok, user.password_code} else false -> {:error, :no_such_password_code} end end @validity_time 3600 - def date_difference([password_code | _tail]) do + def date_difference(password_code) do if NaiveDateTime.diff(NaiveDateTime.utc_now(), password_code.inserted_at) <= @validity_time and NaiveDateTime.diff(NaiveDateTime.utc_now(), password_code.inserted_at) >= 0 do true diff --git a/apps/lenra/test/lenra/services/password_services_test.exs b/apps/lenra/test/lenra/services/password_services_test.exs new file mode 100644 index 00000000..cc151bd9 --- /dev/null +++ b/apps/lenra/test/lenra/services/password_services_test.exs @@ -0,0 +1,24 @@ +defmodule LenraServers.PasswordServicesTest do + @moduledoc """ + Test the Password services + """ + + use Lenra.RepoCase, async: true + + alias Lenra.{PasswordServices, Repo} + + test "The password code should be deleted after password modification" do + {:ok, %{inserted_user: user}} = register_john_doe() + {:ok, _} = PasswordServices.send_password_code(user) + user! = Repo.preload(user, [:password_code]) + password_code = user!.password_code + assert not is_nil(password_code) + + assert {:ok, ^password_code} = PasswordServices.check_password_code_valid(user!, password_code.code) + assert {:ok, _} = PasswordServices.update_lost_password(user!, password_code, %{"password" => "MyNewPassword42!"}) + + # Force reload the password code + user! = Repo.preload(user!, [:password_code], force: true) + assert is_nil(user!.password_code) + end +end diff --git a/apps/lenra_web/lib/lenra_web/controllers/user_controller.ex b/apps/lenra_web/lib/lenra_web/controllers/user_controller.ex index e1b2bb6f..791fbf1b 100644 --- a/apps/lenra_web/lib/lenra_web/controllers/user_controller.ex +++ b/apps/lenra_web/lib/lenra_web/controllers/user_controller.ex @@ -82,8 +82,8 @@ defmodule LenraWeb.UserController do def password_lost_modification(conn, params) do with {:ok, user} <- get_user_with_email(params["email"]), - {:ok, _} <- PasswordServices.check_password_code_valid(user, params["code"]), - {:ok, _password} <- PasswordServices.update_lost_password(user, params) do + {:ok, password_code} <- PasswordServices.check_password_code_valid(user, params["code"]), + {:ok, _password} <- PasswordServices.update_lost_password(user, password_code, params) do reply(conn) end end @@ -94,6 +94,9 @@ defmodule LenraWeb.UserController do _error -> nil end + # This is an intended behavior. + # If the email does not exists, we should not return an error to the client. + # Otherwise it gives an information to hackers and allow brutforce reply(conn) end end diff --git a/apps/lenra_web/test/controllers/user_controller_test.exs b/apps/lenra_web/test/controllers/user_controller_test.exs index aa7c5b54..2851248b 100644 --- a/apps/lenra_web/test/controllers/user_controller_test.exs +++ b/apps/lenra_web/test/controllers/user_controller_test.exs @@ -229,6 +229,55 @@ defmodule LenraWeb.UserControllerTest do assert Map.has_key?(data, "access_token") end + @tag :auth_user + test "Using lost password code twice should fail the second time", %{conn: conn} do + new_password = "New@password42" + new_password2 = "New@password1337" + + # Ask for a lost password code + conn! = + post( + conn, + Routes.user_path(conn, :password_lost_code, %{ + "email" => @john_doe_user_params["email"] + }) + ) + + # Retrive the code (not returned by the controller) + user = Repo.get_by(User, email: @john_doe_user_params["email"]) + password_code = Repo.get_by(PasswordCode, user_id: user.id) + + # Change password first time + conn! = + put( + conn!, + Routes.user_path(conn!, :password_lost_modification, %{ + "email" => @john_doe_user_params["email"], + "code" => password_code.code, + "password" => new_password, + "password_confirmation" => new_password + }) + ) + + # First one should succeed + assert %{"success" => true} = json_response(conn!, 200) + + # Change password a second time with another password but the same code + conn! = + put( + conn!, + Routes.user_path(conn!, :password_lost_modification, %{ + "email" => @john_doe_user_params["email"], + "code" => password_code.code, + "password" => new_password2, + "password_confirmation" => new_password2 + }) + ) + + # Second one should fail + assert %{"success" => false} = json_response(conn!, 400) + end + test "change lost password wrong email test", %{conn: conn} do post(conn, Routes.user_path(conn, :register, @john_doe_user_params))