diff --git a/.tool-versions b/.tool-versions
index fb3f44dc7..dbc4e5140 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
-elixir 1.10.3-otp-22
-erlang 22.3.3
+elixir 1.10.3-otp-23
+erlang 23.0
nodejs 10.20.1
diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex
index 794575654..178e64445 100644
--- a/apps/fg_http/lib/fg_http/devices/device.ex
+++ b/apps/fg_http/lib/fg_http/devices/device.ex
@@ -11,6 +11,7 @@ defmodule FgHttp.Devices.Device do
schema "devices" do
field :name, :string
field :public_key, :string
+ field :ifname, :string
field :last_ip, EctoNetwork.INET
has_many :rules, Rule
@@ -22,7 +23,7 @@ defmodule FgHttp.Devices.Device do
@doc false
def changeset(device, attrs) do
device
- |> cast(attrs, [:last_ip, :user_id, :name, :public_key])
- |> validate_required([:user_id])
+ |> cast(attrs, [:last_ip, :ifname, :user_id, :name, :public_key])
+ |> validate_required([:user_id, :ifname, :name, :public_key])
end
end
diff --git a/apps/fg_http/lib/fg_http/sessions.ex b/apps/fg_http/lib/fg_http/sessions.ex
index 3d1f7d325..de3cb808d 100644
--- a/apps/fg_http/lib/fg_http/sessions.ex
+++ b/apps/fg_http/lib/fg_http/sessions.ex
@@ -6,7 +6,7 @@ defmodule FgHttp.Sessions do
import Ecto.Query, warn: false
alias FgHttp.Repo
- alias FgHttp.Sessions.Session
+ alias FgHttp.{Sessions.Session, Users.User}
@doc """
Returns the list of sessions.
@@ -51,7 +51,7 @@ defmodule FgHttp.Sessions do
"""
def create_session(attrs \\ %{}) do
%Session{}
- |> Session.changeset(attrs)
+ |> Session.create_changeset(attrs)
|> Repo.insert()
end
@@ -85,10 +85,32 @@ defmodule FgHttp.Sessions do
{:error, %Ecto.Changeset{}}
"""
- def delete_session(%Session{} = session) do
+ def delete_session(%Session{} = session, really_delete: true) do
Repo.delete(session)
end
+ def delete_session(%Session{} = session) do
+ update_session(session, %{deleted_at: DateTime.utc_now()})
+ end
+
+ def load_session(_session_id) do
+ query =
+ from s in Session,
+ where: is_nil(s.deleted_at),
+ join: u in User,
+ on: s.user_id == u.id,
+ select: {s, u},
+ preload: :user
+
+ case session = Repo.one(query) do
+ nil ->
+ {:error, "Valid session not found"}
+
+ _ ->
+ {:ok, session}
+ end
+ end
+
@doc """
Returns an `%Ecto.Changeset{}` for tracking session changes.
diff --git a/apps/fg_http/lib/fg_http/sessions/session.ex b/apps/fg_http/lib/fg_http/sessions/session.ex
index 506e6075c..bc191d2a8 100644
--- a/apps/fg_http/lib/fg_http/sessions/session.ex
+++ b/apps/fg_http/lib/fg_http/sessions/session.ex
@@ -6,18 +6,49 @@ defmodule FgHttp.Sessions.Session do
use Ecto.Schema
import Ecto.Changeset
- alias FgHttp.{Users.User}
+ alias FgHttp.{Users, Users.User}
schema "sessions" do
+ field :deleted_at, :utc_datetime
belongs_to :user, User
+ # VIRTUAL FIELDS
+ field :user_email, :string, virtual: true
+ field :user_password, :string, virtual: true
+
timestamps()
end
@doc false
def changeset(session, attrs \\ %{}) do
session
- |> cast(attrs, [])
+ |> cast(attrs, [:deleted_at])
|> validate_required([])
end
+
+ def create_changeset(session, attrs \\ %{}) do
+ session
+ |> cast(attrs, [:user_email, :user_password])
+ |> validate_required([:user_email, :user_password])
+ |> authenticate_user()
+ end
+
+ defp authenticate_user(
+ %Ecto.Changeset{
+ valid?: true,
+ changes: %{user_email: email, user_password: password}
+ } = changeset
+ ) do
+ user = Users.get_user!(email: email)
+
+ case User.authenticate_user(user, password) do
+ {:ok, _} ->
+ change(changeset, user_id: user.id)
+
+ {:error, error_msg} ->
+ raise("There was an issue with your password: #{error_msg}")
+ end
+ end
+
+ defp authenticate_user(changeset), do: changeset
end
diff --git a/apps/fg_http/lib/fg_http/users.ex b/apps/fg_http/lib/fg_http/users.ex
index 5fb224803..323dd66b3 100644
--- a/apps/fg_http/lib/fg_http/users.ex
+++ b/apps/fg_http/lib/fg_http/users.ex
@@ -35,6 +35,14 @@ defmodule FgHttp.Users do
** (Ecto.NoResultsError)
"""
+ def get_user!(email: email) do
+ Repo.get_by!(User, email: email)
+ end
+
+ def get_user!(reset_token: reset_token) do
+ Repo.get_by!(User, reset_token: reset_token)
+ end
+
def get_user!(id), do: Repo.get!(User, id)
@doc """
@@ -51,7 +59,7 @@ defmodule FgHttp.Users do
"""
def create_user(attrs \\ %{}) do
%User{}
- |> User.changeset(attrs)
+ |> User.create_changeset(attrs)
|> Repo.insert()
end
@@ -69,7 +77,7 @@ defmodule FgHttp.Users do
"""
def update_user(%User{} = user, attrs) do
user
- |> User.changeset(attrs)
+ |> User.update_changeset(attrs)
|> Repo.update()
end
diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex
index 2df6319fe..b01f6f987 100644
--- a/apps/fg_http/lib/fg_http/users/user.ex
+++ b/apps/fg_http/lib/fg_http/users/user.ex
@@ -11,20 +11,79 @@ defmodule FgHttp.Users.User do
schema "users" do
field :email, :string
field :confirmed_at, :utc_datetime
+ field :reset_sent_at, :utc_datetime
+ field :reset_token, :string
field :last_signed_in_at, :utc_datetime
- field :password_digest, :string
+ field :password_hash, :string
- has_many :devices, Device
- has_many :sessions, Session
+ # VIRTUAL FIELDS
+ field :password, :string, virtual: true
+ field :password_confirmation, :string, virtual: true
+ field :current_password, :string, virtual: true
+
+ has_many :devices, Device, on_delete: :delete_all
+ has_many :sessions, Session, on_delete: :delete_all
timestamps()
end
@doc false
- def changeset(user, attrs \\ %{}) do
+ def create_changeset(user, attrs \\ %{}) do
user
- |> cast(attrs, [:email, :confirmed_at, :password_digest, :last_signed_in_at])
- |> validate_required([:email])
+ |> cast(attrs, [:email, :password_hash, :password, :password_confirmation])
+ |> validate_required([:email, :password, :password_confirmation])
|> unique_constraint(:email)
+ |> put_password_hash()
+ |> validate_required([:password_hash])
end
+
+ # Only password being updated
+ def update_changeset(
+ user,
+ %{
+ user: %{
+ password: _password,
+ password_confirmation: _password_confirmation,
+ current_password: _current_password
+ }
+ } = attrs
+ ) do
+ user
+ |> cast(attrs, [:email, :password, :password_confirmation, :current_password])
+ |> verify_current_password(attrs[:current_password])
+ |> validate_required([:password, :password_confirmation, :current_password])
+ |> put_password_hash()
+ |> validate_required([:password_hash])
+ end
+
+ # Only email being updated
+ def update_changeset(user, %{user: %{email: _email}} = attrs) do
+ user
+ |> cast(attrs, [:email])
+ |> validate_required([:email])
+ end
+
+ def changeset(%__MODULE__{} = _user, _attrs \\ %{}) do
+ change(%__MODULE__{})
+ end
+
+ def authenticate_user(user, password_candidate) do
+ Argon2.check_pass(user, password_candidate)
+ end
+
+ defp verify_current_password(user, current_password) do
+ {:ok, user} = authenticate_user(user, current_password)
+ user
+ end
+
+ defp put_password_hash(
+ %Ecto.Changeset{
+ valid?: true,
+ changes: %{password: password}
+ } = changeset
+ ) do
+ change(changeset, password_hash: Argon2.hash_pwd_salt(password))
+ end
+
+ defp put_password_hash(changeset), do: changeset
end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
index 97aab5114..1bc4abb9b 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
@@ -6,7 +6,7 @@ defmodule FgHttpWeb.DeviceController do
use FgHttpWeb, :controller
alias FgHttp.{Devices, Devices.Device}
- plug FgHttpWeb.Plugs.Authenticator
+ plug FgHttpWeb.Plugs.SessionLoader
def index(conn, _params) do
devices = Devices.list_devices(conn.assigns.current_user.id)
@@ -18,13 +18,14 @@ defmodule FgHttpWeb.DeviceController do
render(conn, "new.html", changeset: changeset)
end
- def create(conn, %{"device" => device_params}) do
- create_params = %{
+ def create(conn, %{"device" => %{"public_key" => _public_key} = device_params}) do
+ our_params = %{
"user_id" => conn.assigns.current_user.id,
- "name" => "Auto"
+ "name" => "Default",
+ "ifname" => "wg0"
}
- all_params = Map.merge(device_params, create_params)
+ all_params = Map.merge(device_params, our_params)
case Devices.create_device(all_params) do
{:ok, device} ->
diff --git a/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex
new file mode 100644
index 000000000..f5531b7db
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex
@@ -0,0 +1,39 @@
+defmodule FgHttpWeb.PasswordResetController do
+ @moduledoc """
+ Implements the CRUD for password resets
+ """
+
+ use FgHttpWeb, :controller
+ alias FgHttp.{Users, Users.User}
+
+ plug FgHttpWeb.Plugs.RedirectAuthenticated
+
+ def new(conn, _params) do
+ conn
+ |> render("new.html", changeset: User.changeset(%User{}))
+ end
+
+ # Don't actually create anything. Instead, update the user with a reset token and send
+ # the password reset email.
+ def create(conn, %{
+ "password_reset" =>
+ %{
+ reset_token: reset_token,
+ password: _password,
+ password_confirmation: _password_confirmation,
+ current_password: _current_password
+ } = user_params
+ }) do
+ user = Users.get_user!(reset_token: reset_token)
+
+ case Users.update_user(user, user_params) do
+ {:ok, _user} ->
+ conn
+ |> render("success.html")
+
+ {:error, changeset} ->
+ conn
+ |> render("new.html", changeset: changeset)
+ end
+ end
+end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
index 8ad21d26c..deb6df527 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
@@ -6,7 +6,7 @@ defmodule FgHttpWeb.RuleController do
use FgHttpWeb, :controller
alias FgHttp.{Devices, Rules, Rules.Rule}
- plug FgHttpWeb.Plugs.Authenticator
+ plug FgHttpWeb.Plugs.SessionLoader
def index(conn, %{"device_id" => device_id}) do
device = Devices.get_device!(device_id, with_rules: true)
diff --git a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex
index c2cd30e7a..9196d8046 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex
@@ -3,11 +3,11 @@ defmodule FgHttpWeb.SessionController do
Implements the CRUD for a Session
"""
+ alias FgHttp.{Sessions, Sessions.Session}
use FgHttpWeb, :controller
- alias FgHttp.{Repo, Sessions.Session, Users.User}
- plug :redirect_authenticated when action in [:new]
- plug FgHttpWeb.Plugs.Authenticator when action in [:delete]
+ plug FgHttpWeb.Plugs.RedirectAuthenticated when action in [:new]
+ plug FgHttpWeb.Plugs.SessionLoader when action in [:delete]
# GET /sessions/new
def new(conn, _params) do
@@ -18,12 +18,13 @@ defmodule FgHttpWeb.SessionController do
# Sign In
# POST /sessions
- def create(conn, params) do
- changeset = Session.changeset(%Session{}, params)
-
- case Repo.insert(changeset) do
+ def create(conn, %{"session" => session_params}) do
+ case Sessions.create_session(session_params) do
{:ok, session} ->
conn
+ # Prevent session fixation
+ |> clear_session()
+ |> put_session(:session_id, session.id)
|> assign(:current_session, session)
|> put_flash(:info, "Session created successfully")
|> redirect(to: Routes.device_path(conn, :index))
@@ -31,29 +32,21 @@ defmodule FgHttpWeb.SessionController do
{:error, changeset} ->
conn
|> put_flash(:error, "Error creating session.")
- |> render("new.html", changeset: changeset)
+ |> render("new.html", changeset: changeset, user_signed_in?: false)
end
end
# Sign Out
# DELETE /session
def delete(conn, _params) do
- case Repo.delete(conn.current_session) do
+ session = conn.assigns.current_session
+
+ case Sessions.delete_session(session) do
{:ok, _session} ->
conn
- |> assign(:current_session, nil)
- |> put_flash(:info, "Session deleted successfully.")
+ |> clear_session
+ |> put_flash(:info, "Signed out successfully.")
|> redirect(to: "/")
end
end
-
- defp redirect_authenticated(conn, _) do
- user = %User{id: 1, email: "dev_user@fireguard.network"}
- session = %Session{user_id: user.id}
-
- conn
- |> assign(:current_session, session)
- |> redirect(to: "/")
- |> halt()
- end
end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex
index 602fec260..a3912bbf4 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex
@@ -4,9 +4,9 @@ defmodule FgHttpWeb.UserController do
"""
use FgHttpWeb, :controller
- alias FgHttp.{Repo, Users, Users.User}
+ alias FgHttp.{Users, Users.User}
- plug FgHttpWeb.Plugs.Authenticator when action in [:show, :edit, :update, :delete]
+ plug FgHttpWeb.Plugs.SessionLoader when action in [:show, :edit, :update, :delete]
# GET /users/new
def new(conn, _params) do
@@ -46,9 +46,7 @@ defmodule FgHttpWeb.UserController do
# PATCH /user
def update(conn, params) do
- changeset = User.changeset(conn.current_user, params)
-
- case Repo.update(changeset) do
+ case Users.update_user(conn.current_user, params) do
{:ok, user} ->
conn
|> assign(:current_user, user)
@@ -64,7 +62,7 @@ defmodule FgHttpWeb.UserController do
# DELETE /user
def delete(conn, _params) do
- case Repo.delete(conn.current_user) do
+ case Users.delete_user(conn.current_user) do
{:ok, _user} ->
conn
|> assign(:current_user, nil)
diff --git a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex
index 8e68ccf47..8a8137b67 100644
--- a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex
+++ b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex
@@ -15,13 +15,13 @@ defmodule FgHttpWeb.NewDeviceLive do
{:ok, assign(socket, :device, device)}
end
- defp wait_for_device_connect(_socket) do
- # XXX: pass socket to fg_vpn somehow
- :timer.send_after(3000, self(), :update)
+ # XXX: Receive other device details to create an intelligent name
+ def handle_info({:pubkey, pubkey}, socket) do
+ device = %Device{public_key: pubkey}
+ {:noreply, assign(socket, :device, device)}
end
- def handle_info(:update, socket) do
- new_device = Map.merge(socket.assigns.device, %{public_key: "foobar"})
- {:noreply, assign(socket, :device, new_device)}
+ defp wait_for_device_connect(_socket) do
+ :timer.send_after(3000, self(), {:pubkey, "foobar"})
end
end
diff --git a/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex b/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex
index 1bdcfd0a9..5ff1ed6cc 100644
--- a/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex
+++ b/apps/fg_http/lib/fg_http_web/live/new_device_live.html.leex
@@ -42,8 +42,7 @@ Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
<%=
link("<- Something's wrong. Don't add this device and go back.",
- to: Routes.device_path(@socket, :index),
- method: :delete)
+ to: Routes.device_path(@socket, :index))
%>
<% end %>
diff --git a/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex b/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex
deleted file mode 100644
index 53c347906..000000000
--- a/apps/fg_http/lib/fg_http_web/plugs/authenticator.ex
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule FgHttpWeb.Plugs.Authenticator do
- @moduledoc """
- Loads the user's session from cookie
- """
-
- import Plug.Conn
- alias FgHttp.{Repo, Users.User}
-
- def init(default), do: default
-
- def call(conn, _default) do
- user = Repo.one(User)
- assign(conn, :current_user, user)
- end
-end
diff --git a/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex b/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex
new file mode 100644
index 000000000..ff99a1aba
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex
@@ -0,0 +1,21 @@
+defmodule FgHttpWeb.Plugs.RedirectAuthenticated do
+ @moduledoc """
+ Redirects users when he/she tries to access an open resource while authenticated.
+ """
+
+ import Plug.Conn
+ import Phoenix.Controller, only: [redirect: 2]
+
+ def init(default), do: default
+
+ def call(conn, _default) do
+ if get_session(conn, :session_id) do
+ conn
+ |> redirect(to: "/")
+ |> halt()
+ else
+ conn
+ |> assign(:user_signed_in?, false)
+ end
+ end
+end
diff --git a/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex
new file mode 100644
index 000000000..b6966a2e5
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex
@@ -0,0 +1,27 @@
+defmodule FgHttpWeb.Plugs.SessionLoader do
+ @moduledoc """
+ Loads the user's session from cookie
+ """
+
+ import Plug.Conn
+ import Phoenix.Controller, only: [redirect: 2]
+ alias FgHttp.Sessions
+ alias FgHttpWeb.Router.Helpers, as: Routes
+
+ def init(default), do: default
+
+ def call(conn, _default) do
+ case Sessions.load_session(get_session(conn, :session_id)) do
+ {:ok, {session, user}} ->
+ conn
+ |> assign(:current_session, session)
+ |> assign(:current_user, user)
+ |> assign(:user_signed_in?, true)
+
+ {:error, _} ->
+ conn
+ |> redirect(to: Routes.session_path(conn, :new))
+ |> halt
+ end
+ end
+end
diff --git a/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex b/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex
index b2ff5a818..a2366b5bb 100644
--- a/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex
@@ -1,5 +1,11 @@
New Device
+<%= if assigns[:changeset] do %>
+ The following errors occurred when creating this Device:
+
+ <%= aggregated_errors(@changeset) %>
+<% end %>
+
<%= live_render(@conn, FgHttpWeb.NewDeviceLive, session: %{"current_user_id" => @conn.assigns.current_user.id}) %>