Authentication

Authenticate a user

The API spec for authentication is as follows:

HTTP verb URL Required fields
POST /api/users/login email, password

Example request body:

{
  "user":{
    "email": "jake@jake.jake",
    "password": "jakejake"
  }
}

Example response body:

{
  "user": {
    "email": "jake@jake.jake",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": null,
    "image": null
  }
}

Example failure response body:

{
  "errors": {
    "email or password": [
      "is invalid"
    ]
  }
}

The successful login response includes a JSON Web Token (JWT). This token is included in the HTTP headers on subsequent requests to authorize the user’s actions. We’ll use Guardian to authenticate users and take advantage of its support for JWT tokens.

An authentication framework for use with Elixir applications.

Guardian

Guardian provides a number of Plug modules to include within the Phoenix request handling pipeline. We’ll make use of the following three plugs for the Conduit API:

Plug Usage
Guardian.Plug.VerifyHeader Looks for a token in the Authorization header.
  Useful for APIs.
  If one is not found, this does nothing.
Guardian.Plug.EnsureAuthenticated Looks for a previously verified token.
  If one is found, continues.
  Otherwise it will call the :unauthenticated function of your handler.
Guardian.Plug.LoadResource Looks in the sub field of the token,
  fetches the resource from the configured serializer
  and makes it available via Guardian.Plug.current_resource(conn).

In mix.exs, add the guardian package as a dependency:

defp deps do
  [
    # ...
    {:guardian, "~> 0.14"},
  ]
end

Fetch and compile the mix dependencies:

$ mix do deps.get, deps.compile

Guardian requires a secret key to be generated for our application. We can use the “secret generator” mix task provided by Phoenix to do this:

$ mix phx.gen.secret
IOjbrty1eMEBzc5aczQn0FR4Gd8P9IF1cC7tqwB7ThV/uKjS5mrResG1Y0lCzTNJ

Configure Guardian in config/config.exs, including copying the key from above into secret_key:

config :guardian, Guardian,
  allowed_algos: ["HS512"],
  verify_module: Guardian.JWT,
  issuer: "Conduit",
  ttl: {30, :days},
  allowed_drift: 2000,
  verify_issuer: true,
  secret_key: "IOjbrty1eMEBzc5aczQn0FR4Gd8P9IF1cC7tqwB7ThV/uKjS5mrResG1Y0lCzTNJ",
  serializer: Conduit.Auth.GuardianSerializer

Guardian requires you to implement a serializer, as specified in the config above, to encode and decode your resources into and out of the JWT token. The only resource we’re interested in are users. We can encode the user’s UUID into the token, and later use it to fetch the user projection from the read model.

At this point we will move the existing Conduit.Auth module into its own context. This will allows us to keep authentication concerns, such as password hashing, separate from user accounts.

The Guardian serializer module is created at lib/conduit/auth/guardian_serializer.ex:

# lib/conduit/auth/guardian_serializer.ex
defmodule Conduit.Auth.GuardianSerializer do
  @moduledoc """
  Used by Guardian to serialize a JWT token
  """

  @behaviour Guardian.Serializer

  alias Conduit.Accounts
  alias Conduit.Accounts.Projections.User

  def for_token(%User{} = user), do: {:ok, "User:#{user.uuid}"}
  def for_token(_), do: {:error, "Unknown resource type"}

  def from_token("User:" <> uuid), do: {:ok, Accounts.user_by_uuid(uuid)}
  def from_token(_), do: {:error, "Unknown resource type"}
end

We need to add the user_by_uuid/1 function to the accounts context:

defmodule Conduit.Accounts do
  @doc """
  Get a single user by their UUID
  """
  def user_by_uuid(uuid) when is_binary(uuid) do
    Repo.get(User, uuid)
  end
end

The Conduit API specs show the authentication header is in the following format:

Authorization: Token jwt.token.here

So we need to prefix the JWT token with the word Token. To do this we configure the Phoenix web router, in lib/conduit_web/router.ex, and instruct Guardian to use Token as the realm:

defmodule ConduitWeb.Router do
  use ConduitWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug Guardian.Plug.VerifyHeader, realm: "Token"
    plug Guardian.Plug.LoadResource
  end

  # ... routes omitted
end

We will create a new session controller to support user login. It will authenticate the user from the provided email and password and return the user’s details as JSON:

# lib/conduit_web/controllers/session_controller.ex
defmodule ConduitWeb.SessionController do
  use ConduitWeb, :controller

  alias Conduit.Auth
  alias Conduit.Accounts.Projections.User

  action_fallback ConduitWeb.FallbackController

  def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
    case Auth.authenticate(email, password) do
      {:ok, %User{} = user} ->
        conn
        |> put_status(:created)
        |> render(ConduitWeb.UserView, "show.json", user: user)

      {:error, :unauthenticated} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(ConduitWeb.ValidationView, "error.json", errors: %{"email or password" => ["is invalid"]\
})
    end
  end
end

An error is returned with a 422 HTTP status code and a generic “is invalid” error message for the email or password on login failure. The existing user and validation views are reused for rendering the response as JSON.

The session controller uses a new public function in the auth context: authenticate/2

This function will look for an existing user by their email address, and then compare their stored hashed password with the password provided hashed using the same bcrypt hash function. An {:error, :unauthenticated} tagged tuple is returned on failure:

# lib/conduit/auth/auth.ex
defmodule Conduit.Auth do
  @moduledoc """
  Boundary for authentication.
  Uses the bcrypt password hashing function.
  """

  alias Comeonin.Bcrypt

  alias Conduit.Accounts
  alias Conduit.Accounts.Projections.User

  def authenticate(email, password) do
    with {:ok, user} <- user_by_email(email) do
      check_password(user, password)
   else
     reply -> reply
   end
  end

  def hash_password(password), do: Bcrypt.hashpwsalt(password)
  def validate_password(password, hash), do: Bcrypt.checkpw(password, hash)

  defp user_by_email(email) do
    case Accounts.user_by_email(email) do
      nil -> {:error, :unauthenticated}
      user -> {:ok, user}
    end
  end

  defp check_password(%User{hashed_password: hashed_password} = user, password) do
    case validate_password(password, hashed_password) do
      true -> {:ok, user}
      _ -> {:error, :unauthenticated}
    end
  end
end

The POST /api/users/login action, mapped to the new session controller, is added to the router:

defmodule ConduitWeb.Router do
  use ConduitWeb, :router

  # ... pipeline omitted

  scope "/api", ConduitWeb do
    pipe_through :api

    post "/users/login", SessionController, :create
    post "/users", UserController, :create
  end
end

With the controller and routing configured we can write a web integration test to verify the functionality. In test/conduit_web/controllers/session_controller_test.exs we use the Phoenix connection test case to access helper functions for controllers.

There are three scenarios to test:

  1. Successfully authenticating an existing user and valid password.
  2. Failing to authenticate a known user when the password is incorrect.
  3. Failing to authenticate an unknown user.
# test/conduit/web/controllers/session_controller_test.exs
defmodule ConduitWeb.SessionControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "authenticate user" do
    @tag :web
    test "creates session and renders session when data is valid", %{conn: conn} do
      register_user()

      conn = post conn, session_path(conn, :create), user: %{
        email: "jake@jake.jake",
        password: "jakejake"
      }

      assert json_response(conn, 201)["user"] == %{
        "bio" => nil,
        "email" =>
        "jake@jake.jake",
        "image" => nil,
        "username" => "jake",
      }
    end

    @tag :web
    test "does not create session and renders errors when password does not match", %{conn: conn} do
      register_user()

      conn = post conn, session_path(conn, :create), user: %{
        email: "jake@jake.jake",
        password: "invalidpassword"
      }

      assert json_response(conn, 422)["errors"] == %{
        "email or password" => [
          "is invalid"
        ]
      }
    end

    @tag :web
    test "does not create session and renders errors when user not found", %{conn: conn} do
      conn = post conn, session_path(conn, :create), user: %{
        email: "doesnotexist",
        password: "jakejake"
      }

      assert json_response(conn, 422)["errors"] == %{
        "email or password" => [
          "is invalid"
        ]
      }
    end
  end

  defp register_user, do: fixture(:user)
end

Run the new web tests, mix test --only web, to confirm that our changes are good.

Generating a JWT token

User authentication is now working, but we’ve omitted a necessary part of the user data returned as JSON from the login and register user actions. In both cases our response does not include the JWT token as shown in the example response:

{
  "user": {
    "email": "jake@jake.jake",
    "token": "jwt.token.here",
    "username": "jake"
  }
}

We need to rectify that omission by including the token in the response. First, we’ll include the token property in the session controller test. We assert that it is not empty when successfully authenticating a user:

defmodule ConduitWeb.SessionControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "authenticate user" do
    @tag :web
    test "creates session and renders session when data is valid", %{conn: conn} do
      register_user()

      conn = post conn, session_path(conn, :create), user: %{
        email: "jake@jake.jake",
        password: "jakejake"
      }
      json = json_response(conn, 201)["user"]
      token = json["token"]

      assert json == %{
        "bio" => nil,
        "email" => "jake@jake.jake",
        "token" => token,
        "image" => nil,
        "username" => "jake",
      }
      refute token == ""
    end
  end
end

Let’s use Guardian to generate the token for us. It will use the Conduit.Auth.GuardianSerializer module we’ve already written and configured to serialize our user resource into a string for inclusion in the token.

To generate the JWT we use Guardian.encode_and_sign/2 by adding a Conduit.JWT module and wrapper function in lib/conduit_web/jwt.ex:

# lib/conduit_web/jwt.ex
defmodule ConduitWeb.JWT do
  @moduledoc """
  JSON Web Token helper functions, using Guardian
  """

  def generate_jwt(resource, type \\ :token) do
    case Guardian.encode_and_sign(resource, type) do
      {:ok, jwt, _full_claims} -> {:ok, jwt}
    end
  end
end

Since the token generation will be used in both the session and user controllers we will import the ConduitWeb.JWT module in the Phoenix controller macro, in lib/conduit_web/web.ex. This makes the generate_jwt/2 function available to use in all of our web controllers.

defmodule ConduitWeb do
  def controller do
    quote do
      use Phoenix.Controller, namespace: ConduitWeb
      import Plug.Conn
      import ConduitWeb.Router.Helpers
      import ConduitWeb.Gettext
      import ConduitWeb.JWT
    end
  end

  # ... view, router, and channel definitions omitted
end

The session controller needs to be updated to generate the JWT after authenticating the user. The JWT token is passed to the render function to make it available to the view:

# lib/conduit_web/controllers/session_controller.ex
defmodule ConduitWeb.SessionController do
  use ConduitWeb, :controller

  alias Conduit.Auth
  alias Conduit.Accounts.Projections.User

  action_fallback ConduitWeb.FallbackController

  def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
    with {:ok, %User{} = user} <- Auth.authenticate(email, password),
         {:ok, jwt} <- generate_jwt(user) do
       conn
        |> put_status(:created)
        |> render(ConduitWeb.UserView, "show.json", user: user, jwt: jwt)
    else
      {:error, :unauthenticated} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(ConduitWeb.ValidationView, "error.json", errors: %{"email or password" => ["is invalid"]\
})
    end
  end
end

The render function in the user view for a single user merges the JWT token into the user data that is rendered as JSON:

# lib/conduit_web/views/user_view.ex
defmodule ConduitWeb.UserView do
  use ConduitWeb, :view
  alias ConduitWeb.UserView

  def render("index.json", %{users: users}) do
    %{users: render_many(users, UserView, "user.json")}
  end

  def render("show.json", %{user: user, jwt: jwt}) do
    %{user: user |> render_one(UserView, "user.json") |> Map.merge(%{token: jwt})}
  end

  def render("user.json", %{user: user}) do
    %{
      username: user.username,
      email: user.email,
      bio: user.bio,
      image: user.image,
    }
  end
end

Running the web tests again, mix test --only web, confirms that the token is successfully generated and included in the response.

Getting the current user

An authenticated HTTP GET request to /api/user should return a JSON representation of the current user. Authentication is determined by the presence of a valid HTTP request header containing the JWT token: Authorization: Token jwt.token.here.

We will start by adding two new tests to the user controller to verify the following scenarios:

  1. Successful authentication, with a valid JWT token, returns the current user as JSON data.
  2. An invalid request, missing a JWT token, returns a 401 Unauthorized response.

To support a valid request we must register a user and generate a JWT token for them in the test setup. The token is included in the request headers of the test connection using the authenticated_conn/1 function:

defmodule ConduitWeb.UserControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "get current user" do
    @tag :web
    test "should return user when authenticated", %{conn: conn} do
      conn = get authenticated_conn(conn), user_path(conn, :current)
      json = json_response(conn, 200)["user"]
      token = json["token"]

      assert json == %{
        "bio" => nil,
        "email" => "jake@jake.jake",
        "token" => token,
        "image" => nil,
        "username" => "jake",
      }
      refute token == ""
    end

    @tag :web
    test "should not return user when unauthenticated", %{conn: conn} do
      conn = get conn, user_path(conn, :current)

      assert response(conn, 401) == ""
    end
  end

  def authenticated_conn(conn) do
    with {:ok, user} <- fixture(:user),
         {:ok, jwt} <- ConduitWeb.JWT.generate_jwt(user)
    do
      conn
      |> put_req_header("authorization", "Token " <> jwt)
    end
  end
end

The failing tests guide us towards our next code change, we need to register the /api/user route in the router:

defmodule ConduitWeb.Router do
  use ConduitWeb, :router

  # ... pipeline omitted

  scope "/api", ConduitWeb do
    pipe_through :api

    get "/user", UserController, :current
    post "/users/login", SessionController, :create
    post "/users", UserController, :create
  end
end

Next we add a current function to the user controller module. Before doing so we’ll take advantage of Guardian’s built in support for Phoenix controllers. Using the Guardian.Phoenix.Controller module in our controller provides easier access to the current user and their claims. The public controller functions are extended to accept two additional parameters, user and claims, as shown below.

Before: def current(conn, params) do

After: def current(conn, params, user, claims) do

We will also use two plugs provided by Guardian:

  1. Guardian.Plug.EnsureAuthenticated ensures a verified token exists.
  2. Guardian.Plug.EnsureResource guards against a resource not found.

Both plugs require us to implement an error handler module that deals with failure cases. In lib/conduit_web/error_handler.ex we provide functions for the three main error cases. They each return an appropriate HTTP error status code and an empty response body:

# lib/conduit_web/error_handler.ex
defmodule ConduitWeb.ErrorHandler do
  import Plug.Conn

  @doc """
  Return 401 for "Unauthorized" requests

  A request requires authentication but it isn't provided
  """
  def unauthenticated(conn, _params), do: respond_with(conn, :unauthorized)

  @doc """
  Return 403 for "Forbidden" requests

  A request may be valid, but the user doesn't have permissions to perform the action
  """
  def unauthorized(conn, _params), do: respond_with(conn, :forbidden)

  @doc """
  Return 401 for "Unauthorized" requests

  A request requires authentication but the resource has not been found
  """
  def no_resource(conn, _params), do: respond_with(conn, :unauthorized)

  defp respond_with(conn, status) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, "")
  end
end

The Guardian plugs are configured with our error handler module and to only apply to the current controller action. This action returns the authenticated current user, and their JWT token, as JSON data:

# lib/conduit_web/controllers/user_controller.ex
defmodule ConduitWeb.UserController do
  use ConduitWeb, :controller
  use Guardian.Phoenix.Controller

  alias Conduit.Accounts
  alias Conduit.Accounts.Projections.User

  action_fallback ConduitWeb.FallbackController

  plug Guardian.Plug.EnsureAuthenticated, %{handler: ConduitWeb.ErrorHandler} when action in [:current]
  plug Guardian.Plug.EnsureResource, %{handler: ConduitWeb.ErrorHandler} when action in [:current]

  def create(conn, %{"user" => user_params}, _user, _claims) do
    with {:ok, %User{} = user} <- Accounts.register_user(user_params),
         {:ok, jwt} = generate_jwt(user) do
      conn
      |> put_status(:created)
      |> render("show.json", user: user, jwt: jwt)
    end
  end

  def current(conn, _params, user, _claims) do
    jwt = Guardian.Plug.current_token(conn)

    conn
    |> put_status(:ok)
    |> render("show.json", user: user, jwt: jwt)
  end
end

When an unauthenticated user requests /api/user the Guardian.Plug.EnsureAuthenticated plug will step in. It redirects the request to our error handler module, which responds with a 401 unauthorized status code.

Run the web tests, mix test --only web, to confirm the new route is working as per the API spec.

We’ve now built out the basic user registration and authentication features required for Conduit. Let’s move on to authoring articles in the next chapter.