In the previous article, we have implemented authentication solution. But we still have some spots to cover. Our users still can access every page even though they are not logged in.

Today we will introduce some improvements in our authentication solution and do it by using the Plugs.

Let’s get started.

What is a Plug?

Plugs are some sort of layers in a Phoenix application. Which can be injected in between initial request and final response. They are small reusable prices and can be used to transform the connection.

Every Phoenix request starts with a connection and then goes deep down by slightly changing it on its way.

So Plug is some piece of code which receives a connection, changes it slightly and returns back.

Types of the Plugs: module and function

There are two types of plugs in the Phoenix. They are module plugs and function plugs. We are going to cover both of them in this article.

The structure of the Module plug is pretty simple. It is a module with two functions is in it:

  • ‘init/1’ - prepares the arguments (if needed) to be passed to call/2
  • call/2 - accepts the connection, transforms it and returns it back

It would be easier to understand when we will implement them.

Set current user

At the moment in our application, we are keeping the current_user_id in the session and only access it through Prater.Auth.current_user/1 functions. The connection itself does not know about a current user.

Let’s implement a functionality, that on every page request we are going to get the user ID from the session and assign the user itself to the connection.

Let’s create a new file lib/prater_web/plugs/set_current_user.ex for our future plug.

There is no strict rule where to keep plugs in your application, but it seems to me, keeping them in the same directory with controllers makes more sense. Thus I’m going to keep plugs in the lib/prater_web/plugs directory.

And the implementation of the plug looks like:

defmodule PraterWeb.Plugs.SetCurrentUser do
  import Plug.Conn

  alias Prater.Repo
  alias Prater.Auth.User

  def init(_params) do
  end

  def call(conn, _params) do
    user_id = Plug.Conn.get_session(conn, :current_user_id)

    cond do
      current_user = user_id && Repo.get(User, user_id) ->
        conn
        |> assign(:current_user, current_user)
        |> assign(:user_signed_in?, true)
      true ->
        conn
        |> assign(:current_user, nil)
        |> assign(:user_signed_in?, false)
    end
  end
end

So that is a module plug and in order to behave like a plug, we need to import Plug.Conn module to extend its functionality. Then we have init/1 function, which in our case does nothing.

The rest of the stuff happens in the call/2. The first thing we do is we retrieve the user ID from the session. Then in the cond construction, we are trying to fetch the user with that ID from the DataBase. If we succeeded we are assigning the user map to the :current_user of the connection and we also assign :user_signed_in? to true, because we know that the user is signed in. If we are not succeeded we assign a nil to :current_user and false to :user_signed_in?.

That is pretty much it that does the plug do. If the user signed in, we are updating the connection so it would know about a user. And we set it to nil otherwise.

Now we need to start using the plug somehow.

Before we do that we can ask ourselves a question: “When do we need that functionality to be triggered?”. You ask the same question for every plug you are trying to implement. The answer helps to figure out next steps.

In our case, we want this plug to be used everywhere in the browser. So the Router would be the right place to plug it.

In the lib/prater_web/router.ex you may see the pipelines already and even the other plugs which come with the Phoenix. We want our Plug to work in the browser (we don’t care about API for now). That means we need to inject it into pipeline :browser as follows.

pipeline :browser do
  # ...
  plug PraterWeb.Plugs.SetCurrentUser
end

That’s it. The plug has been plugged in and works.

To see that in action we need to replace the following condition in the layout file (lib/prater_web/templates/layout/app.html.eex):

<%= if Prater.Auth.user_signed_in?(@conn) do %>
  <nav class="my-2 my-md-0 mr-md-3">
    Signed in as: <strong><%= Prater.Auth.current_user(@conn).username %></strong>

with the:

<%= if @user_signed_in? do %>
  <nav class="my-2 my-md-0 mr-md-3">
    Signed in as: <strong><%= @current_user.username %></strong>

We can access the assigns of the connection in several ways:

  • We can access it directly by using @ sign in the views. As we did with the @user_signed_in?
  • We can access by calling conn.assigns[:user_signed_in?]
  • Or we can access by calling conn.assigns.user_signed_in?

You decide which approach to use depending on your situation.

We can also remove the following functions, as soon as we are not using them anymore.

Prater.Auth.current_user/1
Prater.Auth.user_signed_in?/1

Now if you run the page you can see that the sign in and sign out buttons are rendered properly. That means our plug works.

Authenticate user

Let’s make a quick look at our application, we have the authentication functionality in place, but our guests can access every page without being signed in. That is not how we want it to be.

Let’s restrict guests from access rooms’ pages except for the index page.

As a first solution, we can implement a function plug to do the job.

On top of our RoomController let’s start with plugging the plug.

plug :authenticate_user when action in [:new, :create, :show, :edit, :update, :delete]

Here we are describing that we would like to use the :authenticate_user plus as a function. Also, we want it to be activated only for the following actions.

Let’s jump to the implementation. Define the private function right inside that controller.

defp authenticate_user(conn, _params) do
  if conn.assigns.user_signed_in? do
    conn
  else
    conn
    |> put_flash(:error, "You need to sign in or sign up before continuing.")
    |> redirect(to: session_path(conn, :new))
    |> halt()
  end
end

What do we do here? We checked if the user signed in and if he is, we just the return the connection back without doing anything else.

If we have a guest visiting a page, then we set a flash message, redirect him to a sign in page and also halting the connection in order to prevent rest of the plugs to be invoked.

If you try to access a new room page now being signed out you should see the work of the plug in action.

A tiny piece before we move on.

You may notice that we have provided a long list of actions to use our plug.

plug :authenticate_user when action in [:new, :create, :show, :edit, :update, :delete]

In fact, those are all available actions except index. Can we somehow make the list shorter? Yes, we can.

We can describe which actions to avoid instead by:

plug :authenticate_user when action not in [:index]

Ok. The functionality works fine. But we are probably want to use the same functionality in other controllers. We don’t have them for now, but anyway, you will probably have them in another application.

We don’t want to implement the same function plug in every controller. So for that case, the module plug would work better. Let’s refactor that.

Extract authenticate user into Module Plug

As soon as it would be a module we need a new file for that: lib/prater_web/plugs/authenticate_user.ex with the following content:

defmodule PraterWeb.Plugs.AuthenticateUser do
  import Plug.Conn
  import Phoenix.Controller

  alias PraterWeb.Router.Helpers

  def init(_params) do
  end

  def call(conn, _params) do
    if conn.assigns.user_signed_in? do
      conn
    else
      conn
      |> put_flash(:error, "You need to sign in or sign up before continuing.")
      |> redirect(to: Helpers.session_path(conn, :new))
      |> halt()
    end
  end
end

All in all that is the same function we have in the RoomController with small changes. On top, if importing the Plug.Conn module we also need to import the Phoenix.Controller module, because we have some stuff related to controllers functionality. We also need to use Router.Helpers module in order to call session_path function.

That is it. Nothing unusual.

The last piece we need to change to glue all together is the way how we are plugging that plug.

We need to change the following line

plug :authenticate_user when action not in [:index]

to use module name instead

plug PraterWeb.Plugs.AuthenticateUser when action not in [:index]

Ah yes, and we can remove RoomController.authenticate_user function. We don’t need it anymore.

That’s it. It works and we can reuse it for different controllers.

Authorize user

Now we have our pages somehow secured from the guests. The guests simply cannot access them. But signed in users can manage every room in the app. That means they can change and remove rooms event though those rooms do not belong to them.

Let’s go further and allow users to manage rooms they created.

As usual, there are some prerequisites to implement that task. At the moment we are not keeping the track of which room belongs to which user. We need to link them together before we can proceed.

Define association between users and rooms

To link rooms and user’s together we need to update our database and keep the track of user_id in the rooms table. Because we want a room to belong to a user. Also, a user can create several rooms.

For every change we do in the database we need to do those using migrations.

 mix ecto.gen.migration add_user_id_to_rooms
* creating priv/repo/migrations
* creating priv/repo/migrations/20180225104638_add_user_id_to_rooms.exs

and open the created file *_add_user_id_to_rooms.exs (for you it will have a different timestamp in the file name) and update the change function.

defmodule Prater.Repo.Migrations.AddUserIdToRooms do
  use Ecto.Migration

  def change do
    alter table(:rooms) do
      add :user_id, references(:users)
    end
  end
end

We want to alter the table rooms and define user_id column to be the reference to a users table.

We can execute the migration afterward.

→ mix ecto.migrate
[info] == Running Prater.Repo.Migrations.AddUserIdToRooms.change/0 forward
[info] alter table rooms
[info] == Migrated in 0.0s

We have user_id column now, but our models still do not aware about that relation.

In the Room model we need to update the schema and explicitly say that the room belongs to a user, and a user is being represented by Prater.Auth.User model.

schema "rooms" do
  # ...
  belongs_to :user, Prater.Auth.User
  # ...
end

The similar to the User model. A user can own many rooms. A room is represented by Prater.Conversation.Room model.

schema "users" do
  # ...
  has_many :rooms, Prater.Conversation.Room
  # ...
end

Now models know about associations between them. The next thing is we need to associate the room we are going to create with the current user.

When we create our room in the RoomController.create action we are hiding all the logic inside Conversation.create_room function. That function does not know anything about the current user. So we need to update the function definition and pass the user inside.

We need to change the way how we call it.

case Conversation.create_room(room_params) do

to

case Conversation.create_room(conn.assigns.current_user, room_params) do

Then we need to update the function itself and associate a room with a current user. We can achieve that using a Ecto.build_assoc/2 function.

That is how our function should look like before and after.

# Before
def create_room(attrs \\ %{}) do
  %Room{}
  |> Room.changeset(attrs)
  |> Repo.insert()
end

# After
def create_room(user, attrs \\ %{}) do
  user
  |> Ecto.build_assoc(:rooms)
  |> Room.changeset(attrs)
  |> Repo.insert()
end

Instead of empty %Room{} map, first, we are building an association and then do the rest by passing it into changeset.

That is it. Go and create a room.

Once you’ve done it you can try to fetch the room from the iex session:

iex> Prater.Repo.get(Prater.Conversation.Room, 11)
[debug] QUERY OK source="rooms" db=1.2ms
SELECT r0."id", r0."description", r0."name", r0."topic", r0."user_id", r0."inserted_at", r0."updated_at" FROM "rooms" AS r0 WHERE (r0."id" = $1) [11]
%Prater.Conversation.Room{
  __meta__: #Ecto.Schema.Metadata<:loaded, "rooms">,
  description: nil,
  id: 11,
  inserted_at: ~N[2018-02-25 11:42:53.589542],
  name: "User's room",
  topic: nil,
  updated_at: ~N[2018-02-25 11:42:53.593703],
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: 10
}

We can see now how it associated with the user user_id: 10 and has a user association as well.

That’s it. Now we can proceed with the Authorization Plug.

In that case, we would like to check if a user can or cannot access the rooms. So the function plug would be a valid option to use, we are not going to reuse it, other controllers.

Let’s start with enabling the plug. We want it to work only for the edit, update, and delete actions.

plug :authorize_user when action in [:edit, :update, :delete]

And the implementation itself.

defp authorize_user(conn, _params) do
  %{params: %{"id" => room_id}} = conn
  room = Conversation.get_room!(room_id)

  if conn.assigns.current_user.id == room.user_id do
    conn
  else
    conn
    |> put_flash(:error, "You are not authorized to access that page")
    |> redirect(to: room_path(conn, :index))
    |> halt()
  end
end

Here the _params are not the same as the params we get in the actions, so we need to fetch Room ID manually. If we working with one of the edit, update, or delete actions we have that in URL param: /rooms/:id. All we need is to Pattern Match it from our connection.

%{params: %{"id" => room_id}} = conn

Then we need to find the room

room = Conversation.get_room!(room_id)

and then check if current user ID matches with the user_id field of the Room.

Then we check if a user is the owner of a room, then he can proceed. Others will the flash messages and will be redirected to the rooms index page.

That is all we need to make that plug work.

Yes, in that example we are fetching the room from the DataBase twice. Which we usually should avoid. But let’s leave it for now as it is.

Or you can have a homework and implement yet another plug function to fetch the room and assign it to the connection.

Small improvements

Most likely we would like to check the same “can access” (conn.assigns.current_user.id == room.user_id) condition in other places. For example to hide “Edit” and “Delete” buttons from the page or even have more improvement logic to check if a user can manage it or not.

Let’s extract it into a separate module and keep it in lib/prater/auth/authorizer.ex:

defmodule Prater.Auth.Authorizer do
  def can_manage?(user, room) do
    user && user.id == room.user_id
  end
end

Now we replace

if conn.assigns.current_user.id == room.user_id do

with

if Authorizer.can_manage?(conn.assigns.current_user, room) do

and describe an alias on top of the controller

alias Prater.Auth.Authorizer

Now we can hide the “Edit” and “Delete” buttons if a current user cannot manage a room:

Open the lib/prater_web/templates/room/show.html.eex file and wrap buttons with the condition:

<%= if Prater.Auth.Authorizer.can_manage?(@current_user, @room) do %>
  <div>
    <%= link "Edit", to: room_path(@conn, :edit, @room.id), class: "btn btn-default" %>
    <%= link "Delete", to: room_path(@conn, :delete, @room), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger" %>
  </div>
<% end %>

That’s it. Now the manage buttons can see only an owner of a room.

Wrapping up

This time we have learned about plugs in Phoenix. Which might be scary from the beginning, but pretty easy to use. We know there are two types of plugs such as module plug and function plug. We have also learned how to use plugs inside the router and inside controllers.

The source code of the current implementation you can find on GitHub page.

See you next time.