Refactoring Faster Than You Can Spell Phoenix

Written by: Micah Woods

Plug is a fantastic tool, and Phoenix is built on top of it! In my last blog post, we added a way to create sessions and tokens for authentication. However, we didn't actually authenticate anything in our API. This time, we're going to build a Plug that checks for an API token and inserts the current user into our application.

Creating a Plug

The anatomy of a plug is simple. If you come from the Ruby world, you can think of it like Rack. And luckily, we have already seen plugs used in our router.

plug :accepts, ["json"]

This is an example of a function Plug. A function plug takes two arguments, a Plug.Conn struct and options. In order for the plug chain to continue, the function must also return a Plug.Conn. You can find the definition of accepts/2 here.

The other form a Plug can take is a module. In order to use the module form, you must define two functions: init/1 and call/2. The init/1 is used to provide the options (the second argument) to the call/2 function.

Once again, the first argument to call/2 is the Plug.Conn, and the function must return a Plug.Conn for chaining. For more information about defining plugs, check out the README.

Creating Our Plug

Now that we know how to create a plug, let’s build one for our authentication layer. The system we built so far allows us to create a session in the database and returns the client a token. According to the spec for HTTP token access, we need a header that looks like this: Authorization: Token token="yourtokenhere". So let’s build a plug that checks for that token.

As always, we will start with a test. We have three scenarios:

  1. A token was not provided.

  2. An invalid token was provided.

  3. A valid token was provided.

In the first two scenarios, we simply need to respond with a 401 status code. In the last example, we want to add the current user to the conn, so that later down the plug chain (in our controller), we can use it. As always, let’s start with some tests.

defmodule TodoApi.AuthenticationTest do
  use TodoApi.ConnCase
  alias TodoApi.{Authentication, Repo, User, Session}
  @opts Authentication.init([])
  def put_auth_token_in_header(conn, token) do
    conn
    |> put_req_header("authorization", "Token token=\"#{token}\"")
  end
  test "finds the user by token", %{conn: conn} do
    user = Repo.insert!(%User{})
    session = Repo.insert!(%Session{token: "123", user_id: user.id})
    conn = conn
    |> put_auth_token_in_header(session.token)
    |> Authentication.call(@opts)
    assert conn.assigns.current_user
  end
  test "invalid token", %{conn: conn} do
    conn = conn
    |> put_auth_token_in_header("foo")
    |> Authentication.call(@opts)
    assert conn.status == 401
    assert conn.halted
  end
  test "no token", %{conn: conn} do
    conn = Authentication.call(conn, @opts)
    assert conn.status == 401
    assert conn.halted
  end
end

As you can see, all three of our scenarios are outlined and tested. There are two things to note particularly. First, we must ensure that the conn is halted; otherwise it will keep chaining. Second, we cached @opts from the init/1 function, in case we ever want to do something with them in the future.

Now for the fun part:

defmodule TodoApi.Authentication do
  import Plug.Conn
  alias TodoApi.{Repo, User, Session}
  import Ecto.Query, only: [from: 2]
  def init(options), do: options
  def call(conn, _opts) do
    case find_user(conn) do
      {:ok, user} -> assign(conn, :current_user, user)
      _otherwise  -> auth_error!(conn)
    end
  end
  defp find_user(conn) do
    with auth_header = get_req_header(conn, "authorization"),
         {:ok, token}   <- parse_token(auth_header),
         {:ok, session} <- find_session_by_token(token),
    do:  find_user_by_session(session)
  end
  defp parse_token(["Token token=" <> token]) do
    {:ok, String.replace(token, "\"", "")}
  end
  defp parse_token(_non_token_header), do: :error
  defp find_session_by_token(token) do
    case Repo.one(from s in Session, where: s.token == ^token) do
      nil     -> :error
      session -> {:ok, session}
    end
  end
  defp find_user_by_session(session) do
    case Repo.get(User, session.user_id) do
      nil  -> :error
      user -> {:ok, user}
    end
  end
  defp auth_error!(conn) do
    conn |> put_status(:unauthorized) |> halt()
  end
end

Our call/2 function has a case statement. If find_user/1 returns {:ok, user}, then we assign the current user to the conn. Any other return value will put the 401 status and halt the Plug chain.

Notice we also used a rather new feature: with. This is brand new to Elixir (1.2.4). It's a lot like a pipeline except anytime the thing on the right does not match the thing on the left, the pipeline is stopped, and the thing on the right is returned. This sounds confusing, so let’s look at an easier example.

def bar, do: :error
def baz, do: IO.puts("will never get executed")
with {:ok, foo} <- bar do
  baz
end

In this example, the bar/0 function returns :error, so the pipeline stops and :error is returned; baz/0 is never called. If the bar/0 function had returned {:ok, :whatever}, the pipeline would have continued, and baz/0 would have been called. AWESOME!

Refactor Our Controller

Now that we have a plug to do authentication, let's modify the todo controller to use it. In the following example, we are only going to modify the index and create actions. I'll leave the rest to you.

Change the todo controller tests:

defmodule TodoApi.TodoControllerTest do
  use TodoApi.ConnCase
  alias TodoApi.Todo
  alias TodoApi.User
  alias TodoApi.Session
  @valid_attrs %{complete: true, description: "some content"}
  @invalid_attrs %{}
  setup %{conn: conn} do
    user = create_user(%{name: "jane"})
    session = create_session(user)
    conn = conn
    |> put_req_header("accept", "application/json")
    |> put_req_header("authorization", "Token token=\"#{session.token}\"")
    {:ok, conn: conn, current_user: user }
  end
  def create_user(%{name: name}) do
    User.changeset(%User{}, %{email: "#{name}@example.com"}) |> Repo.insert!
  end
  def create_session(user) do
    # in the last blog post I had a copy-paste error
    # so you may need to use Session.registration_changeset
    Session.create_changeset(%Session{user_id: user.id}, %{}) |> Repo.insert!
  end
  def create_todo(%{description: _description, owner_id: _owner_id} = options) do
    Todo.changeset(%Todo{}, options) |> Repo.insert!
  end
  test "lists all entries on index", %{conn: conn, current_user: current_user} do
    create_todo(%{description: "our first todo", owner_id: current_user.id})
    another_user = create_user(%{name: "johndoe"})
    create_todo(%{description: "thier first todo", owner_id: another_user.id})
    conn = get conn, todo_path(conn, :index)
    assert Enum.count(json_response(conn, 200)["data"]) == 1
    assert %{"description" => "our first todo"} = hd(json_response(conn, 200)["data"])
  end
  test "creates and renders resource when data is valid", %{conn: conn, current_user: current_user} do
    conn = post conn, todo_path(conn, :create), todo: @valid_attrs
    assert json_response(conn, 201)["data"]["id"]
    todo = Repo.get_by(Todo, @valid_attrs)
    assert todo
    assert todo.owner_id == current_user.id
  end
end

The main changes are:

  1. We created some helper functions for sessions, users, and todos.

  2. We modified our setup function to pass in the current user.

  3. We modified our tests to pattern match the current user so we can assert against it.

  4. We asserted that we only see our todos.

  5. We asserted that new todos belong to the current user.

Now let's get the tests passing. First create a migration:

$ mix ecto.gen.migration add_owner_id_to_todos
defmodule TodoApi.Repo.Migrations.AddOwnerIdToTodos do
  use Ecto.Migration
  def change do
    alter table(:todos) do
      add :owner_id, references(:users)
    end
  end
end

Add owner_id to the todo required fields:

# web/models/todo.ex
@required_fields ~w(description complete owner_id)

Notice at this point our todo model tests are failing. WOOT! That means they are working. Now add an integer to represent the owner_id in our todo model tests:

@valid_attrs %{complete: true, description: "some content", owner_id: 1}

Finally, let’s modify the controller:

# web/models/todo_controller.ex
defmodule TodoApi.TodoController do
  use TodoApi.Web, :controller
  alias TodoApi.Todo
  plug :scrub_params, "todo" when action in [:create, :update]
  plug TodoApi.Authentication
  def index(conn, _params) do
    user_id = conn.assigns.current_user.id
    query = from t in Todo, where: t.owner_id == ^user_id
    todos = Repo.all(query)
    render(conn, "index.json", todos: todos)
  end
  def create(conn, %{"todo" => todo_params}) do
    changeset = Todo.changeset(
      %Todo{owner_id: conn.assigns.current_user.id}, todo_params
    )
    case Repo.insert(changeset) do
      {:ok, todo} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", todo_path(conn, :show, todo))
        |> render("show.json", todo: todo)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(TodoApi.ChangesetView, "error.json", changeset: changeset)
    end
  end
end

Woohoo, the tests pass! In this example, we added plug TodoApi.Authentication directly to our controller, but we could have created a new plug pipeline and added it there. If we had more controllers that needed authentication, this may be a better solution.

We also changed the index and create actions to use the current user which is now assigned in our plug. Another strategy could be to override the action/2 function in our controller and pass the current user as the third argument to our actions. I will leave that as an exercise for you. Reading this will help you get started.

Conclusion

Plug is awesome -- it allowed us to create a middleware that we essentially used as a before filter. When a request was made, we filtered our params and either returned a 401 status or set the user. Then we refactored our controller to use the plug.

And we did all of this in very little code. Code that is reusable. Code that is easy to understand. Small, understandable, and reusable code translates to blazing fast productivity.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.