Authentication & Authorization

Phoenix authentication with phx.gen.auth, session management, and authorization patterns

Phoenix provides built-in authentication scaffolding via phx.gen.auth and a plug-based architecture for authorization.

phx.gen.auth

The quickest way to add authentication to a Phoenix project:

mix phx.gen.auth Accounts User users

This generates:

File Purpose
accounts.ex Context module for user accounts
user.ex User schema
user_token.ex Token schema for session/reset/confirm tokens
user_session_controller.ex Session controller
user_session_html.ex Login page template
user_registration_controller.ex Registration controller
user_registration_html.ex Registration page template
user_reset_password_controller.ex Password reset controller
user_confirmation_controller.ex Email confirmation controller
user_auth.ex Authentication plugs
routes Auth routes in router

Generated routes

Path Method Purpose
/users/register GET/POST Registration
/users/log_in GET/POST Login
/users/log_out DELETE Logout
/users/settings GET/POST Change email/password
/users/reset_password GET/POST Request password reset
/users/reset_password/:token GET/POST Reset password
/users/confirm POST Resend confirmation
/users/confirm/:token GET Confirm account

Auth plugs

The generated UserAuth module provides plugs:

defmodule MyAppWeb.UserAuth do
  use MyAppWeb, :verification

  @doc "Logs the user in by storing the user in the session."
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    conn
    |> renew_session()
    |> put_session(:user_token, token)
    |> maybe_assign_remember_me(user, params)
  end

  @doc "Logs the user out."
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    if user_token, do: Accounts.delete_session_token(user_token)
    conn
    |> renew_session()
    |> configure_session(renew: true)
    |> redirect(to: "/")
  end

  @doc "Fetches the current user from the session."
  def fetch_current_user(conn, _opts) do
    {user_token, conn} = result(conn)
    user = user_token && Accounts.get_user_by_session_token(user_token)
    assign(conn, :current_user, user)
  end

  @doc "Redirects if user is 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 "Requires authentication."
  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
end

Protecting routes

In the router

scope "/", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  live "/settings", UserSettingsLive
  live "/dashboard", DashboardLive
end

scope "/", MyAppWeb do
  pipe_through [:browser, :redirect_if_user_is_authenticated]

  live "/users/register", UserRegistrationLive
  live "/users/log_in", UserLoginLive
end

In a controller or LiveView

# Controller
defmodule MyAppWeb.DashboardController do
  use MyAppWeb, :controller
  plug :require_auth

  def index(conn, _params), do: render(conn, :index)

  defp require_auth(conn, _) do
    if conn.assigns[:current_user] do
      conn
    else
      conn |> redirect(to: ~p"/users/log_in") |> halt()
    end
  end
end

# LiveView — mount/3
def mount(_params, _session, socket) do
  if socket.assigns[:current_user] do
    {:ok, socket}
  else
    {:ok, redirect(socket, to: ~p"/users/log_in")}
  end
end

Authorization (role-based)

Authorization is separate from authentication. A common pattern:

defmodule MyAppWeb.Plugs.Authorize do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, roles: roles) do
    user = conn.assigns[:current_user]

    if user && user.role in roles do
      conn
    else
      conn
      |> put_status(:forbidden)
      |> put_view(html: MyAppWeb.ErrorHTML)
      |> render("403.html")
      |> halt()
    end
  end
end

# In the router
scope "/admin", MyAppWeb.Admin do
  pipe_through [:browser, :require_authenticated_user, :authorize, roles: [:admin]]

  resources "/users", UserController
end

Policy-based authorization (Canada-style)

defmodule MyAppWeb.Policy do
  def can?(%{role: :admin}, _action, _resource), do: true
  def can?(%{role: :editor}, :edit, %Post{author_id: id}, id), do: true
  def can?(%{role: :editor}, :edit, _resource), do: false
  def can?(_, _, _), do: false
end

# Usage in LiveView
def handle_event("edit", _, %{assigns: %{current_user: user, post: post}} = socket) do
  if MyAppWeb.Policy.can?(user, :edit, post) do
    {:noreply, push_navigate(socket, to: ~p"/posts/#{post.id}/edit")}
  else
    {:noreply, put_flash(socket, :error, "Not authorized")}
  end
end

Current user in LiveView

The on_mount callback injects the current user into LiveView assigns:

defmodule MyAppWeb.UserAuth do
  def on_mount(:ensure_authenticated, _params, session, socket) do
    case session_user(session) do
      nil ->
        {:halt, redirect(socket, to: ~p"/users/log_in")}

      user ->
        {:cont, assign(socket, :current_user, user)}
    end
  end

  def on_mount(:mount_current_user, _params, session, socket) do
    {:cont, assign_new(socket, :current_user, fn -> session_user(session) end)}
  end
end

# In the router
live_session :authenticated, on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
  live "/dashboard", DashboardLive
end

live_session :default, on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
  live "/", PageLive
end

API authentication (Bearer tokens)

For API endpoints, use token-based authentication:

defmodule MyAppWeb.API.AuthPlug do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         {:ok, user} <- MyApp.Accounts.fetch_user_by_api_token(token) do
      assign(conn, :current_user, user)
    else
      _ ->
        conn
        |> put_status(:unauthorized)
        |> Phoenix.Controller.json(%{error: "Unauthorized"})
        |> halt()
    end
  end
end

# In the router
scope "/api", MyAppWeb.API do
  pipe_through [:api, MyAppWeb.API.AuthPlug]

  resources "/posts", PostController, except: [:new, :edit]
end

Email verification & password reset

The generated auth includes mailer integration stubs. Configure your mailer:

# config/config.exs
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Local

# config/prod.exs
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Mailgun,
  api_key: System.fetch_env!("MAILGUN_API_KEY"),
  domain: System.fetch_env!("MAILGUN_DOMAIN")