How to do Authentication implementation of programming phoenix book using Phoenix v1.7

Hello Everyone,

I’m reading programming phoenix 1.4 book and trying to implement all the code but using phoenix v1.7. Until chapter 4 I got by with ups and downs but finally I could do it using this thread as a guide (my solution you can find it here)

However in chapter 5 dedicated to authentication I got stuck. The last thing I have done are these. Add the authentication to user controller to restrict access to some pages (specifically the list of users :index) to not logged in users

controllers/auth.ex

defmodule RumblWeb.Auth do
  import Plug.Conn
  def init(opts), do: opts

  def call(conn, _opts) do
    user_id = get_session(conn, :user_id)
    user = user_id && Rumbl.Accounts.get_user(user_id)
    assign(conn, :current_user, user)
  end
end

controllers/user_controller.ex

defmodule RumblWeb.UserController do
  use RumblWeb, :controller
  alias Phoenix.LiveView.Plug
  alias Rumbl.Accounts
  alias Rumbl.Accounts.User

  def index(conn, _params) do
    case authenticate(conn) do
      %Plug.Conn{halted: true} = conn ->
        conn

      conn ->
        users = Accounts.list_users()
        render(conn, :index, users: users)
    end
  end

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user(id)
    render(conn, :show, user: user)
  end

  def new(conn, _params) do
    changeset = Accounts.change_registration(%User{}, %{})
    render(conn, :new, changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.register_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "#{user.name} created!")
        |> redirect(to: ~p"/users")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  defp authenticate(conn) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must to be logged in to access that page")
      |> redirect(to: ~p"/users")
      |> halt()
    end
  end
end

I get a compiler error

compiling 1 file (.ex)

warning: RumbleWeb.Auth.call/2 is undefined (module RumbleWeb.Auth is not available or is yet to be defined)
lib/rumbl_web/router.ex

warning: RumbleWeb.Auth.init/1 is undefined (module RumbleWeb.Auth is not available or is yet to be defined)
lib/rumbl_web/router.ex

error: Phoenix.LiveView.Plug.Conn.struct/0 is undefined, cannot expand struct Phoenix.LiveView.Plug.Conn. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code
lib/rumbl_web/controllers/user_controller.ex:9:7

** (CompileError) lib/rumbl_web/controllers/user_controller.ex: cannot compile module RumblWeb.UserController (errors have been logged)
(stdlib 5.2) lists.erl:1706: :lists.mapfoldl_1/3
(stdlib 5.2) lists.erl:1706: :lists.mapfoldl_1/3
lib/rumbl_web/controllers/user_controller.ex:7: (module)

I think the root error is in Authentication Plug (controllers/auth.ex) because when I comment the code in user_controller.ex that call that Plug then I get this error

(UndefinedFunctionError) function RumbleWeb.Auth.init/1 is undefined (module RumbleWeb.Auth is not available)
RumbleWeb.Auth.init()

Can anyone help me? Thanks in advance

Hi,

alias Phoenix.LiveView.Plug

Remove this line, this module doesn’t exist and it affects the resolution of the real Plug.Conn module.

Also:

warning: RumbleWeb.Auth.call/2 is undefined
warning: RumbleWeb.Auth.init/1 is undefined
(UndefinedFunctionError) function RumbleWeb.Auth.init/1 is undefined

defmodule RumblWeb.Auth do vs RumbleWeb.Auth: you didn’t use the same name everywhere. Make sure to use the same name in your router and controllers (change plug RumbleWeb.Auth to plug RumblWeb.Auth) and as your plug module’s name.

Thanks mate @t0t0 you have identified two important things I made:

  1. alias Phoenix.LiveView.Plug ( I don´t know how I added either when)
  2. I mistakenly added RumbleWeb.Auth to the router.ex instead of RumblWeb.Auth

Solved those error now the project compile I can access some pages but I can´t access localhost:4000/users it throws ERR_TOO_REDIRECTS I tested in different browsers and even delete cookies but the error is still there.

Do you have an idea what this could be happened, could it be a cycle error between authenticate private method and index method in the user_controller.ex

Also I have another question. Searching on the Web for authentication in Phoenix I’ve found this https://hexdocs.pm/phoenix/mix_phx_gen_auth.html so Is this the new (maybe not so new) way to build fast authentication process? Is the way I’m doing it following the book examples an old fashion way

I can´t access localhost:4000/users it throws ERR_TOO_REDIRECTS

We don’t have the whole code, make sure your Plug.Conn.put_session/3 happens.

I’ve found this https://hexdocs.pm/phoenix/mix_phx_gen_auth.html so Is this the new (maybe not so new) way to build fast authentication process?

Yes, indeed.

Thanks again @t0t0 for your quick answer.

Well I’ve tried to keep going but now I have errors everywhere I’m gonna share my code and I hope do not bother you all with this challenge, btw I feel is becoming harder.

Thanks you all for your help

router.ex (most important part)

defmodule RumblWeb.Router do
  use RumblWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {RumblWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug RumblWeb.Auth
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", RumblWeb do
    pipe_through :browser

    resources "/users", UserController, only: [:index, :show, :new, :create]
    resources "/sessions", SessionController, only: [:new, :create, :delete]
    get "/", PageController, :home
  end

user_controller.ex

defmodule RumblWeb.UserController do
  use RumblWeb, :controller
  alias Rumbl.Accounts
  alias Rumbl.Accounts.User
  plug :authenticate when action in [:index, :show]

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, :index, users: users)
  end

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user(id)
    render(conn, :show, user: user)
  end

  def new(conn, _params) do
    changeset = Accounts.change_registration(%User{}, %{})
    render(conn, :new, changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.register_user(user_params) do
      {:ok, user} ->
        conn
        |> RumblWeb.Auth.login(user)
        |> put_flash(:info, "#{user.name} created!")
        |> redirect(to: ~p"/users")
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  defp authenticate(conn, _opts) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must to be logged in to access that page")
      |> redirect(to: ~p"/users")
      |> halt()
    end
  end
end

session_controller.ex

defmodule RumblWeb.SessionController do
  use RumblWeb, :controller

  def new(conn, _) do
    render(conn, :new)
  end

  def create(
    conn,
    %{"session" => %{"username" => username, "password" => pass}}
  ) do
    case Rumbl.Accounts.authenticate_by_username_and_pass(username, pass) do
      {:ok, user} ->
        conn
        |> RumblWeb.Auth.login(user)
        |> put_flash(:info, "Welcome back!")
        |> redirect(to: ~p"/")
      {:error, _reason} -> conn
        |> put_flash(:error, "Invalid username/password combination")
        |> render(:new)
      end
    end

  def delete(conn, _) do
    conn
    |> RumblWeb.Auth.logout()
    |> redirect(to: ~p"/")
  end
end

auth.ex

defmodule RumblWeb.Auth do
  import Plug.Conn
  def init(opts), do: opts

  def call(conn, _opts) do
    user_id = get_session(conn, :user_id)
    user = user_id && Rumbl.Accounts.get_user(user_id)
    assign(conn, :current_user, user)
  end

  def login(conn, user) do
    conn
    |> assign(:current_user, user)
    |> put_session(:user_id, user.id)
    |> configure_session(renew: true)
  end

  def logout(conn) do
    configure_session(conn, drop: true)
  end
end

session_html.ex

defmodule RumblWeb.SessionHTML do
  use RumblWeb, :html

  embed_templates "session_html/*"
end

user_html.ex

defmodule RumblWeb.UserHTML do
  use RumblWeb, :html

  embed_templates "user_html/*"

  def first_name(name) do
    name
    |> String.split(" ")
    |> Enum.at(0)
  end
end

user_html/index.html.heex

<h1>Listing Users</h1>
    <.table id="users" rows={@users}>
      <:col :let={user} label="name"><%= first_name(user.name) %> (<%= user.id %>)</:col>
      <:col :let={user} label="View"><.link href={~p"/users/#{user}"} class={[
                                        "rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
                                        "text-sm font-semibold leading-6 text-white active:text-white/80"
                                        ]}>Show User</.link></:col>

    </.table>

user_html/show.html.heex

<.header>
  User
</.header>

<.list>
  <:item title="ID"><%= @user.id %></:item>
  <:item title="Name"><%= @user.name %></:item>
  <:item title="Username"><%= @user.username %></:item>
</.list>

session_html/new.html.heex

<.header>
  Login
</.header>

<.simple_form
  :let={f}
  for={@conn}
  as={:session}
  phx-change="validate"
  action={~p"/sessions"}>

  <.input field={f[:username]} label="Username" />
  <.input field={f[:password]} label="Password" />

  <:actions>
    <.button>Log in</.button>
  </:actions>

</.simple_form>

Apologize for adding so much text. My main errors right know are trying to access localhost:4000/users (list all users) or localhost:4000/users/1 (show an user) both cases without a session I got a ERR_TOO_MANY_REDIRECTS to me seems I’m having a kind of cycle issue.

The other error is trying to access localhost:4000/sessions/new (for logging in and using a new window incognito mode)

protocol Phoenix.HTML.FormData not implemented for %Plug.Conn{adapter: {Bandit.HTTP1.Adapter, :...}, assigns: %{layout: {RumblWeb.Layouts, "app"}, flash: %{}, current_user: nil}, body_params: %{}, cookies: %{}, halted: false, host: "localhost", method: "GET", owner: #PID<0.17659.0>, params: %{}, path_info: ["sessions", "new"], path_params: %{}, port: 4000, private: %{:phoenix_template => "new.html", :phoenix_view => %{"html" => RumblWeb.SessionHTML, "json" => RumblWeb.SessionJSON}, RumblWeb.Router => [], :phoenix_action => :new, :phoenix_layout => %{"html" => {RumblWeb.Layouts, :app}}, :phoenix_controller => RumblWeb.SessionController, :phoenix_endpoint => RumblWeb.Endpoint, :phoenix_format => "html", :phoenix_root_layout => %{"html" => {RumblWeb.Layouts, :root}}, :phoenix_router => RumblWeb.Router, :plug_session_fetch => :done, :plug_session => %{}, :before_send => [#Function<0.71811376/1 in Plug.CSRFProtection.call/2>, #Function<4.6073741/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.76384852/1 in Plug.Session.before_send/2>, #Function<0.54455629/1 in Plug.Telemetry.call/2>, #Function<1.22806296/1 in Phoenix.LiveReloader.before_send_inject_reloader/3>], :phoenix_request_logger => {"request_logger", "request_logger"}}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{}, req_headers: [{"accept-language", "es-ES,es;q=0.9"}, {"accept-encoding", "gzip, deflate, br, zstd"}, {"sec-fetch-dest", "document"}, {"sec-fetch-user", "?1"}, {"sec-fetch-mode", "navigate"}, {"sec-fetch-site", "none"}, {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"}, {"upgrade-insecure-requests", "1"}, {"sec-ch-ua-platform", "\"macOS\""}, {"sec-ch-ua-mobile", "?0"}, {"sec-ch-ua", "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\""}, {"connection", "keep-alive"}, {"host", "localhost:4000"}], request_path: "/sessions/new", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "F7qNKGQBSv2pJN4AANdC"}, {"referrer-policy", "strict-origin-when-cross-origin"}, {"x-content-type-options", "nosniff"}, {"x-download-options", "noopen"}, {"x-frame-options", "SAMEORIGIN"}, {"x-permitted-cross-domain-policies", "none"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil} of type Plug.Conn (a struct). This protocol is implemented for the following type(s): Ecto.Changeset, Map

Any help or idea to keep searching will be welcome.

Just downgrade to Phoenix 1.6 or lower. Attempting to follow a tutorial with a newer version is an exercise in frustration.

When I learned Phoenix (about a year ago), I just stuck with Phoenix 1.6 or lower (I tried doing what you are doing, with little success and much frustration). Once you understand the changes in 1.7, the transition is not difficult to make. But it is a big enough jump that it breaks every older tutorial.

2 Likes

Try changing for={@conn} to for={@conn.params["session"]}

1 Like

Thanks @t0t0 you are an ace it works!!! this solved the session creation issue and also I could test the rest of pages and works really fine. A lot of thanks.

I still have the errors trying to accessing the pages with no session but I’ll keep searching and reviewing what I’ve done I can’t dismiss some others errors done it by me

Hey @arcanemachine believe me that thought has crossed my mind several times already but I’m keeping trying I don’t wanna give up so easy, the problem with this perseverance is I’m not being productive so sooner than later I would need to made a decision between go for phoenix <= 1.6 or go for phoenix 1.7

I have finally found a workaround I just changed the authenticate method for redirect to “/” (or /sessions/new to logging page) in case of trying to access a page that requires to be logged in

my solution

defp authenticate(conn, _opts) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must to be logged in to access that page")
      |> redirect(to: ~p"/")
      |> halt()
    end
  end

is different to book solution but at least I think it doesn’t break what could be the user flow.

Thanks to all

I was in a similar position not so long ago, so I admire your dedication.

Just make sure you’re on the right side of the frustration spectrum… The side where you’re still making progress. :smiley:

3 Likes