Ruby

Refactoring Faster Than You Can Spell Phoenix

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.

Reference: Refactoring Faster Than You Can Spell Phoenix from our WCG partner Micah Woods at the Codeship Blog blog.

Micah Woods

Micah discovered his passion for Ruby and web development in college. Pair programming at Hashrocket allows him to focus on testing and quality while delivering value at a blazing fast rate.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button