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:
- A token was not provided.
- An invalid token was provided.
- 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:
- We created some helper functions for sessions, users, and todos.
- We modified our setup function to pass in the current user.
- We modified our tests to pattern match the current user so we can assert against it.
- We asserted that we only see our todos.
- 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. |