Skip to content

Commit ff23437

Browse files
authored
Merge pull request #1188 from code-corps/security-improvements
Improve pwd reset security
2 parents 4ee8fbe + 27b4a0e commit ff23437

6 files changed

Lines changed: 44 additions & 5 deletions

File tree

config/config.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ config :code_corps, :sentry, CodeCorps.Sentry.Async
8686

8787
config :code_corps, :processor, CodeCorps.Processor.Async
8888

89+
config :code_corps, password_reset_timeout: 3600
90+
8991
# Import environment specific config. This must remain at the bottom
9092
# of this file so it overrides the configuration defined above.
9193
import_config "#{Mix.env}.exs"

lib/code_corps/services/forgot_password.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule CodeCorps.Services.ForgotPasswordService do
88
"""
99
def forgot_password(email) do
1010
with %User{} = user <- Repo.get_by(User, email: email),
11-
{ :ok, %AuthToken{} = %{ value: token } } <- AuthToken.changeset(%AuthToken{}, user) |> Repo.insert
11+
{ :ok, %AuthToken{} = %{ value: token } } <- AuthToken.changeset(%AuthToken{}, user) |> Repo.insert
1212
do
1313
Emails.ForgotPasswordEmail.create(user, token) |> Mailer.deliver_now()
1414
else

lib/code_corps_web/controllers/password_controller.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule CodeCorpsWeb.PasswordController do
99
"""
1010
def forgot_password(conn, %{"email" => email}) do
1111
ForgotPasswordService.forgot_password(email)
12+
conn = Guardian.Plug.sign_out(conn, :default)
1213
conn |> put_status(:ok) |> render("show.json", email: email)
1314
end
1415

lib/code_corps_web/controllers/password_reset_controller.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ defmodule CodeCorpsWeb.PasswordResetController do
1818
- If no, a `422` response will return the error.
1919
"""
2020
def reset_password(conn, %{"token" => reset_token, "password" => _password, "password_confirmation" => _password_confirmation} = params) do
21-
with %AuthToken{user: user} <- AuthToken |> Repo.get_by(%{ value: reset_token }) |> Repo.preload(:user),
22-
{:ok, _} <- Phoenix.Token.verify(CodeCorpsWeb.Endpoint, "user", reset_token, max_age: 3600),
21+
with %AuthToken{user: user} = auth_token <- AuthToken |> Repo.get_by(%{ value: reset_token }) |> Repo.preload(:user),
22+
{:ok, _} <- Phoenix.Token.verify(conn, "user", reset_token, max_age: Application.get_env(:code_corps, :password_reset_timeout)),
2323
{:ok, updated_user} <- user |> User.reset_password_changeset(params) |> Repo.update,
24+
{:ok, _auth_token} <- auth_token |> Repo.delete,
2425
{:ok, auth_token, _claims} = updated_user |> Guardian.encode_and_sign(:token)
2526
do
2627
conn
@@ -29,6 +30,7 @@ defmodule CodeCorpsWeb.PasswordResetController do
2930
|> render("show.json", token: auth_token, user_id: updated_user.id, email: updated_user.email)
3031
else
3132
{:error, %Changeset{} = changeset} -> conn |> put_status(422) |> render(CodeCorpsWeb.ErrorView, :errors, data: changeset)
33+
{:error, _} -> conn |> put_status(:not_found) |> render(CodeCorpsWeb.ErrorView, "404.json")
3234
nil -> conn |> put_status(:not_found) |> render(CodeCorpsWeb.ErrorView, "404.json")
3335
end
3436
end

test/lib/code_corps_web/controllers/password_controller_test.exs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule CodeCorpsWeb.PasswordControllerTest do
66

77
alias CodeCorps.AuthToken
88

9-
test "creates and renders resource when email is valid", %{conn: conn} do
9+
test "Unauthenticated - creates and renders resource when email is valid", %{conn: conn} do
1010
user = insert(:user)
1111
attrs = %{"email" => user.email}
1212
conn = post conn, password_path(conn, :forgot_password), attrs
@@ -18,6 +18,21 @@ defmodule CodeCorpsWeb.PasswordControllerTest do
1818
assert_delivered_email CodeCorps.Emails.ForgotPasswordEmail.create(user, token)
1919
end
2020

21+
@tag :authenticated
22+
test "Authenticated - creates and renders resource when email is valid and removes session", %{conn: conn} do
23+
user = insert(:user)
24+
attrs = %{"email" => user.email}
25+
conn = post conn, password_path(conn, :forgot_password), attrs
26+
response = json_response(conn, 200)
27+
28+
assert response == %{ "email" => user.email }
29+
30+
%AuthToken{value: token} = Repo.get_by(AuthToken, user_id: user.id)
31+
assert_delivered_email CodeCorps.Emails.ForgotPasswordEmail.create(user, token)
32+
33+
refute Guardian.Plug.authenticated?(conn)
34+
end
35+
2136
test "does not create resource and renders 200 when email is invalid", %{conn: conn} do
2237
user = insert(:user)
2338
attrs = %{"email" => "random_email@gmail.com"}

test/lib/code_corps_web/controllers/password_reset_controller_test.exs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@ defmodule CodeCorpsWeb.PasswordResetControllerTest do
22
@moduledoc false
33

44
use CodeCorpsWeb.ApiCase, resource_name: :password_reset
5+
import CodeCorps.TestEnvironmentHelper
56
alias CodeCorps.{User, AuthToken}
67

7-
test "updates user password when data is valid", %{conn: conn} do
8+
test "updates user password when data is valid and deletes auth token model", %{conn: conn} do
89
current_user = insert(:user)
910
{:ok, auth_token} = AuthToken.changeset(%AuthToken{}, current_user) |> Repo.insert
11+
12+
assert AuthToken |> Repo.get(auth_token.id) |> Map.get(:id) == auth_token.id
13+
1014
attrs = %{"token" => auth_token.value, "password" => "123456", "password_confirmation" => "123456"}
1115
conn = post conn, password_reset_path(conn, :reset_password), attrs
1216
response = json_response(conn, 201)
17+
1318
assert response
1419
encrypted_password = Repo.get(User, current_user.id).encrypted_password
20+
1521
assert Comeonin.Bcrypt.checkpw("123456", encrypted_password)
22+
assert AuthToken |> Repo.get(auth_token.id) == nil
1623
end
1724

1825
test "does not create resource and renders errors when password does not match", %{conn: conn} do
@@ -21,6 +28,7 @@ defmodule CodeCorpsWeb.PasswordResetControllerTest do
2128
attrs = %{"token" => auth_token.value, "password" => "123456", "password_confirmation" => "another"}
2229
conn = post conn, password_reset_path(conn, :reset_password), attrs
2330
response = json_response(conn, 422)
31+
2432
assert %{"errors" => [%{"detail" => "Password confirmation passwords do not match"}]} = response
2533
end
2634

@@ -29,6 +37,17 @@ defmodule CodeCorpsWeb.PasswordResetControllerTest do
2937
{:ok, _} = AuthToken.changeset(%AuthToken{}, current_user) |> Repo.insert
3038
attrs = %{"token" => "random token", "password" => "123456", "password_confirmation" => "123456"}
3139
conn = post conn, password_reset_path(conn, :reset_password), attrs
40+
41+
assert json_response(conn, 404)
42+
end
43+
44+
test "does not create resource and renders errors when error in token timeout occurs", %{conn: conn} do
45+
modify_env(:code_corps, password_reset_timeout: 0)
46+
47+
current_user = insert(:user)
48+
{:ok, auth_token} = AuthToken.changeset(%AuthToken{}, current_user) |> Repo.insert
49+
attrs = %{"token" => auth_token.value, "password" => "123456", "password_confirmation" => "123456"}
50+
conn = post conn, password_reset_path(conn, :reset_password), attrs
3251
assert json_response(conn, 404)
3352
end
3453

0 commit comments

Comments
 (0)