| defmodule MedicodeWeb.UserAuth do |
| @moduledoc """ |
| Authentication helpers and plugs for routing and session management. |
| """ |
|
|
| use MedicodeWeb, :verified_routes |
|
|
| import Plug.Conn |
| import Phoenix.Controller |
|
|
| alias Medicode.Accounts |
|
|
| |
| |
| |
| @max_age 60 * 60 * 24 * 60 |
| @remember_me_cookie "_medicode_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_and_current_user_in_session(token, user) |
| |> 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 |
| 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 |
| MedicodeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) |
| end |
|
|
| conn |
| |> renew_session() |
| |> delete_resp_cookie(@remember_me_cookie) |
| |> redirect(to: ~p"/users/log_in") |
| 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 |
| if token = get_session(conn, :user_token) do |
| {token, conn} |
| else |
| conn = fetch_cookies(conn, signed: [@remember_me_cookie]) |
|
|
| if token = conn.cookies[@remember_me_cookie] do |
| user = Accounts.get_user_by_session_token(token) |
| {token, put_token_and_current_user_in_session(conn, token, user)} |
| else |
| {nil, conn} |
| end |
| 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 MedicodeWeb.PageLive do |
| use MedicodeWeb, :live_view |
| |
| on_mount {MedicodeWeb.UserAuth, :mount_current_user} |
| ... |
| end |
| |
| Or use the `live_session` of your router to invoke the on_mount callback: |
| |
| live_session :authenticated, on_mount: [{MedicodeWeb.UserAuth, :ensure_authenticated}] do |
| live "/profile", ProfileLive, :index |
| end |
| """ |
| def on_mount(:mount_current_user, _params, session, socket) do |
| {:cont, mount_current_user(socket, session)} |
| end |
|
|
| def on_mount(:ensure_authenticated, _params, session, socket) do |
| socket = mount_current_user(socket, session) |
|
|
| if socket.assigns.current_user do |
| {:cont, socket} |
| else |
| 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} |
| end |
| end |
|
|
| def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do |
| socket = mount_current_user(socket, session) |
|
|
| if socket.assigns.current_user do |
| {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))} |
| else |
| {:cont, socket} |
| end |
| end |
|
|
| defp mount_current_user(socket, session) do |
| Phoenix.Component.assign_new(socket, :current_user, fn -> |
| if user_token = session["user_token"] do |
| Accounts.get_user_by_session_token(user_token) |
| end |
| end) |
| end |
|
|
| @doc """ |
| Used for routes that require the user to not be authenticated. |
| """ |
| def redirect_if_user_is_authenticated(conn, _opts) do |
| if conn.assigns[:current_user] do |
| conn |
| |> redirect(to: signed_in_path(conn)) |
| |> halt() |
| else |
| conn |
| 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 |
| if conn.assigns[:current_user] do |
| conn |
| else |
| conn |
| |> put_flash(:error, "You must log in to access this page.") |
| |> maybe_store_return_to() |
| |> redirect(to: ~p"/users/log_in") |
| |> halt() |
| end |
| end |
|
|
| defp put_token_and_current_user_in_session(conn, token, user) do |
| conn |
| |> put_session(:user_token, token) |
| |> put_session(:current_user, user) |
| |> 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 |
|
|