aprs.me/lib/aprsme_web/user_auth.ex

237 lines
6.7 KiB
Elixir

defmodule AprsmeWeb.UserAuth do
@moduledoc false
use AprsmeWeb, :verified_routes
import Phoenix.Controller
import Plug.Conn
alias Aprsme.Accounts
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_aprs_web_user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
@doc """
Logs the user in.
It renews the session ID and clears the whole session
to avoid fixation attacks. See the renew_session
function to customize this behaviour.
It also sets a `:live_socket_id` key in the session,
so LiveView sessions are identified and automatically
disconnected on log out. The line can be safely removed
if you are not using LiveView.
"""
def log_in_user(conn, user, params \\ %{}) do
token = Accounts.generate_user_session_token(user)
user_return_to = get_session(conn, :user_return_to)
conn
|> renew_session()
|> put_token_in_session(token)
|> maybe_write_remember_me_cookie(token, params)
|> redirect(to: user_return_to || signed_in_path(conn))
end
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
defp maybe_write_remember_me_cookie(conn, _token, _params), do: conn
# This function renews the session ID and erases the whole
# session to avoid fixation attacks. If there is any data
# in the session you may want to preserve after log in/log out,
# you must explicitly fetch the session data before clearing
# and then immediately set it after clearing, for example:
#
# defp renew_session(conn) do
# preferred_locale = get_session(conn, :preferred_locale)
#
# conn
# |> configure_session(renew: true)
# |> clear_session()
# |> put_session(:preferred_locale, preferred_locale)
# end
#
defp renew_session(conn) do
conn
|> configure_session(renew: true)
|> clear_session()
end
@doc """
Logs the user out.
It clears all session data for safety. See renew_session.
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_user_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
@doc """
Authenticates the user by looking into the session
and remember me token.
"""
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
end
defp ensure_user_token(conn) do
case get_session(conn, :user_token) do
nil -> ensure_user_token_from_cookie(conn)
token -> {token, conn}
end
end
defp ensure_user_token_from_cookie(conn) do
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
case conn.cookies[@remember_me_cookie] do
nil -> {nil, conn}
token -> {token, put_token_in_session(conn, token)}
end
end
@doc """
Handles mounting and authenticating the current_user in LiveViews.
## `on_mount` arguments
* `:mount_current_user` - Assigns current_user
to socket assigns based on user_token, or nil if
there's no user_token or no matching user.
* `:ensure_authenticated` - Authenticates the user from the session,
and assigns the current_user to socket assigns based
on user_token.
Redirects to login page if there's no logged user.
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
Redirects to signed_in_path if there's a logged user.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:
defmodule AprsmeWeb.PageLive do
use AprsmeWeb, :live_view
on_mount {AprsmeWeb.UserAuth, :mount_current_user}
...
end
Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, mount_current_user(session, socket)}
end
def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)
case socket.assigns.current_user do
nil ->
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
{:halt, socket}
_user ->
{:cont, socket}
end
end
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)
case socket.assigns.current_user do
nil -> {:cont, socket}
_user -> {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
end
end
defp mount_current_user(session, socket) do
case session do
%{"user_token" => user_token} ->
Phoenix.Component.assign_new(socket, :current_user, fn ->
Accounts.get_user_by_session_token(user_token)
end)
%{} ->
Phoenix.Component.assign_new(socket, :current_user, fn -> nil end)
end
end
@doc """
Used for routes that require the user to not be authenticated.
"""
def redirect_if_user_is_authenticated(conn, _opts) do
case conn.assigns[:current_user] do
nil ->
conn
_user ->
conn
|> redirect(to: signed_in_path(conn))
|> halt()
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_authenticated_user(conn, _opts) do
case conn.assigns[:current_user] do
nil ->
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/users/log_in")
|> halt()
_user ->
conn
end
end
defp put_token_in_session(conn, token) do
conn
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: ~p"/"
end