Aug 30, 2019

Phoenix API Authentication with Pow

Pow provides an excellent authentication solution for Phoenix HTML applications; but what if you are looking to build out an API? Recently a new API guide has been published, and in this post we’ll have a look at how easy it is to use Pow in the context of an API.

I am constantly impressed with Pow. New features and enhancements are regularily implemented, and a quick glance at the commit history shows it to be a very active project. Like much of the Pow documentation, the newly added API guide is pretty comprehensive and provides a good description of how to get things up and running. I figured an example of using a Pow enabled API might be useful however; so even though we’ll largely be following the guide, we’ll add in some extra endpoints to demonstrate Pow API authentication in action.

Creating our app

For demonstration purposes, we’ll build a very simple REST API. We’ll pretend we are building out a basic (super, super, basic) warehouse inventory system, so we’ll call our application warehouse_api.

Let’s create the API!

Terminal
mix phx.new warehouse_api --no-html --no-webpack

Choose Y when asked to fetch and install dependencies. Now we’ll change into the application directory and create the database.

Terminal
cd warehouse_api
mix ecto.create

We’ll use a Phoenix generator to create a simple inventory context with associated endpoints for managing the products in our warehouse.

Terminal
mix phx.gen.json Inventory Product products name:string quantity:integer

As per the mix output, let’s run the migrations…

Terminal
mix ecto.migrate

… and update the routes.

/lib/warehouse_web_api/router.ex …line 8
scope "/api", WarehouseApiWeb do
  pipe_through :api
  resources "/products", ProductController, except: [:new, :edit]
end

That’s all we need for our basic application, let’s fire things up and hit a few endpoints.

Interacting with the API

I am going to be using Insomnia to send requests to the API, but you can use which-ever REST client you prefer.

First off we need to start the server.

Terminal
mix phx.server

Now let’s see if we can create a product.

We’ll create a new request within Insomnia:

We’ll give the request a name of Create Product; select POST as the request method; and specify JSON as the request body format.

We need to set the URL to localhost:4000/api/products. The request body needs to include a top level product key, and under that, a name and quantity.

{
  "product": {
    "name": "Widget 1",
    "quantity": 143
  }
}

When we send our request via the Send button we’ll see that a product has been created!

Fantastic! Let’s set-up one more API call in Insomnia before moving onto getting Pow up and running. We’ll set-up a call to retrieve all the current products in our warehouse. This will need to be a GET request.

We don’t need a body this time, so we set the URL to localhost:4000/api/products and we’re good to go.

Great, so we have some API endpoints we can play around with, let’s see how to go about securing our API with Pow.

Using Pow within our API

Basic Installation

The first step is to add pow to our list of dependencies.

/mix.exs …line 34
defp deps do
  [
    ...,
    ...,
    {:pow, "~> 1.0.13"}
  ]
end

Now we need to grab our dependencies via mix deps.get.

Terminal
mix deps.get

And install Pow.

Terminal
mix pow.install

We now have some configuration to deal with, but first let’s migrate the database to create the Pow specific tables.

Terminal
mix ecto.migrate

Nice! … onto configuration.

Configuration

Only a small configuration change is needed, we just need to add a Pow configuration section to config.ex. I’ve placed this above the logger configuration section.

/config/config.exs
...
...
# Pow configuration
config :warehouse_api, :pow,
  user: WarehouseApi.Users.User,
  repo: WarehouseApi.Repo

# Configures Elixir's Logger
config :logger, :console,
...
...

That’s it for configuration, now we need some router updates.

Router updates

There are a few things we need to update with our router. First off we need to update the existing api pipeline to include a custom authentication plug. In this way our authentication rules will be run on every request. We’ll create this plug after we’re done updating our router.

We also need to add a new pipeline for routes that require authentication.

/lib/warehouse_web_api/routes.ex
defmodule WarehouseApiWeb.Router do
  use WarehouseApiWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug WarehouseApiWeb.ApiAuthPlug, otp_app: :warehouse_api
  end

  pipeline :api_protected do
    plug Pow.Plug.RequireAuthenticated, error_handler: WarehouseApiWeb.ApiAuthErrorHandler
  end
  ...

In the api_protected pipeline we’re referencing a custom error_handler module which we’ll create after finishing the router updates.

As far as the actual routes go, we need to add some Pow specific routes for user creation and session management.

While we are at it, we’ll also update the routes for our Product API endpoint. We’ll require authentication for any actions on the endpoint other than the listing and viewing of products. We can do this by adding a second api scope that pipes through both :api and :api_protected. Note: we could add the resources "/products", ProductController, only: [:show, :index] routes in the same scope as the Pow routes. I kind of like keeping the Pow routes in a seperate scope, but this is strictly a personal preference.

/lib/warehouse_web_api/routes.ex
...

  scope "/api", WarehouseApiWeb do
    pipe_through :api

    resources "/registration", RegistrationController, singleton: true, only: [:create]
    resources "/session", SessionController, singleton: true, only: [:create, :delete]
    post "/session/renew", SessionController, :renew
  end

  scope "/api", WarehouseApiWeb do
    pipe_through :api
    resources "/products", ProductController, only: [:show, :index]
  end

  scope "/api", WarehouseApiWeb do
    pipe_through [:api, :api_protected]
    resources "/products", ProductController, except: [:new, :edit, :show, :index]
  end
end

That’s it for the router, we now need to go about adding our custom authorization plug, error handler and some Pow specific controllers.

Adding Pow specific plugs and controllers

We need to add an authentication plug, an error handling module and controller modules for both user registration and session management. To accomplish this we’ll be using the code referenced in API guide pretty much verbatim:

Let’s start with the authorization plug.

Terminal
touch lib/warehouse_api_web/api_auth_plug.ex
/lib/warehouse_api_web/api_auth_plug.ex
defmodule WarehouseApiWeb.ApiAuthPlug do
  @moduledoc false
  use Pow.Plug.Base

  alias Plug.Conn
  alias Pow.{Config, Store.CredentialsCache}
  alias PowPersistentSession.Store.PersistentSessionCache

  @impl true
  @spec fetch(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def fetch(conn, config) do
    token = fetch_auth_token(conn)

    config
    |> store_config()
    |> CredentialsCache.get(token)
    |> case do
      :not_found -> {conn, nil}
      user       -> {conn, user}
    end
  end

  @impl true
  @spec create(Conn.t(), map(), Config.t()) :: {Conn.t(), map()}
  def create(conn, user, config) do
    store_config = store_config(config)
    token        = Pow.UUID.generate()
    renew_token  = Pow.UUID.generate()
    conn         =
      conn
      |> Conn.put_private(:api_auth_token, token)
      |> Conn.put_private(:api_renew_token, renew_token)

    CredentialsCache.put(store_config, token, user)
    PersistentSessionCache.put(store_config, renew_token, user.id)

    {conn, user}
  end

  @impl true
  @spec delete(Conn.t(), Config.t()) :: Conn.t()
  def delete(conn, config) do
    token = fetch_auth_token(conn)

    config
    |> store_config()
    |> CredentialsCache.delete(token)

    conn
  end

  @doc """
  Create a new token with the provided authorization token.

  The renewal authorization token will be deleted from the store after the user id has been fetched.
  """
  @spec renew(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def renew(conn, config) do
    renew_token  = fetch_auth_token(conn)
    store_config = store_config(config)
    user_id      = PersistentSessionCache.get(store_config, renew_token)

    PersistentSessionCache.delete(store_config, renew_token)

    load_and_create_session(user_id, conn, config)
  end

  defp load_and_create_session(:not_found, conn, _config), do: {conn, nil}
  defp load_and_create_session(user_id, conn, config) do
    case Pow.Operations.get_by([id: user_id], config) do
      nil  -> {conn, nil}
      user -> create(conn, user, config)
    end
  end

  defp fetch_auth_token(conn) do
    conn
    |> Plug.Conn.get_req_header("authorization")
    |> List.first()
  end

  defp store_config(config) do
    backend = Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)

    [backend: backend]
  end
end

So a fair bit of code, but it is all pretty self explanatory, basically everything to do with managing sessions and tokens is handled in the plug.

Next we’ll create the error handler.

Terminal
touch lib/warehouse_api_web/api_auth_error_handler.ex
/lib/warehouse_api_web/api_auth_error_handler.ex
defmodule WarehouseApiWeb.ApiAuthErrorHandler do
  use WarehouseApiWeb, :controller
  alias Plug.Conn

  @spec call(Conn.t(), :not_authenticated) :: Conn.t()
  def call(conn, :not_authenticated) do
    conn
    |> put_status(401)
    |> json(%{error: %{code: 401, message: "Not authenticated"}})
  end
end

Simple… when we encounter an unauthenticated request for a protected route, we just return a 401 status code and an error message.

Finally the Pow specific controllers.

Terminal
touch lib/warehouse_api_web/controllers/registration_controller.ex
touch lib/warehouse_api_web/controllers/session_controller.ex
/lib/warehouse_api_web/controllers/registration_controller.ex
defmodule WarehouseApiWeb.RegistrationController do
  use WarehouseApiWeb, :controller

  alias Ecto.Changeset
  alias Plug.Conn
  alias WarehouseApiWeb.ErrorHelpers

  @spec create(Conn.t(), map()) :: Conn.t()
  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.create_user(user_params)
    |> case do
      {:ok, _user, conn} ->
        json(conn, %{data: %{token: conn.private[:api_auth_token], renew_token: conn.private[:api_renew_token]}})

      {:error, changeset, conn} ->
        errors = Changeset.traverse_errors(changeset, &ErrorHelpers.translate_error/1)

        conn
        |> put_status(500)
        |> json(%{error: %{status: 500, message: "Couldn't create user", errors: errors}})
    end
  end
end

The registration controller provides a single endpoint for creating new users.

And here is the session controller.

/lib/warehouse_api_web/controllers/session_controller.ex
defmodule WarehouseApiWeb.SessionController do
  use WarehouseApiWeb, :controller

  alias WarehouseApiWeb.ApiAuthPlug

  @spec create(Conn.t(), map()) :: Conn.t()
  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.authenticate_user(user_params)
    |> case do
      {:ok, conn} ->
        json(conn, %{data: %{token: conn.private[:api_auth_token], renew_token: conn.private[:api_renew_token]}})

      {:error, conn} ->
        conn
        |> put_status(401)
        |> json(%{error: %{status: 401, message: "Invalid email or password"}})
    end
  end

  @spec renew(Conn.t(), map()) :: Conn.t()
  def renew(conn, _params) do
    config = Pow.Plug.fetch_config(conn)

    conn
    |> ApiAuthPlug.renew(config)
    |> case do
      {conn, nil} ->
        conn
        |> put_status(401)
        |> json(%{error: %{status: 401, message: "Invalid token"}})

      {conn, _user} ->
        json(conn, %{data: %{token: conn.private[:api_auth_token], renew_token: conn.private[:api_renew_token]}})
    end
  end

  @spec delete(Conn.t(), map()) :: Conn.t()
  def delete(conn, _params) do
    {:ok, conn} = Pow.Plug.clear_authenticated_user(conn)

    json(conn, %{data: %{}})
  end
end

A little more involved, we have 3 functions; one for logging in (create); one for logout (delete); and one for token renewal.

And that’s it! We’re good to go, so let’s see our newly protected API in action.

Checking out the Pow functionality

Let’s fire up the server.

Terminal
mix phx.server

First thing we will do is run our List Products request from Insomnia… since nothing has changed with this route, everything should still be all good:

Nice, still working!

Let’s try creating another product.

No dice… this is what we are expecting as we’ve protected the create route. We’ll need to be authenticated in order to create a new product.

Before logging in, let’s update the create function. We’re just going to add a debug line to inspect the current_user value that Pow makes available to us. Once we log in and create a product we should see a current_user showing up in the console.

/lib/warehouse_api_web/controllers/product_controller.ex …line 14
def create(conn, %{"product" => product_params}) do
  IO.inspect conn.assigns.current_user
  with {:ok, %Product{} = product} <- Inventory.create_product(product_params) do
  ...
  ...

Ok, with that out of the way, let’s register a user. We need to create another POST request, again with a JSON body.

Provide a URL of localhost:4000/api/registration and the following body:

{
  "user": {
    "email": "bob@example.com",
    "password": "foobar1234",
    "confirm_password": "foobar1234"
  }
}

Running the request, gives us a new user:

The registration response also gives us a token we can now use in calling the create product endpoint.

Add a new Authorization Header to the create product request, and fill in the token value from the registration request.

Now running the create product request succeeds!

Let’s double-check everything by logging out the user, ensuring we can’t create a product and then logging in the user and ensuring product creation is again a-ok.

So first to log out, we need to create a DELETE request.

We need to use a URL of localhost:4000/api/session and we need to provide the Authorization header. The header value is how Pow knows which token to invalidate.

Running the endpoint results in an empty response.

What happens now, if we attempt to create a new product?

As expected we now get a 401 error.

Let’s make sure we can log in.

We need to create a POST request.

The URL to use is localhost:4000/api/session, and the JSON body is:

{
  "user": {
    "email": "bob@example.com",
    "password": "foobar1234"
  }
}

Sending the request yields a new token:

Now if we update the Authorization header value in the create product request, we’ll be able to create products again.

Sweet! If we glance at our console we can see we also have programmatic access to the current user.

This is useful if for instance you’ve set up some roles you need programmatic access to.

Summary

That concludes today’s post. Setting up API authentication with Pow turns out to be pretty easy!

I had previously looked at using Pow with an API and wasn’t too sure how to go about it, so it’s awesome to see the documentation and guides continue to evolve.

Note: the API guide also includes some example test modules for testing the api_auth_plug, registration_controller, and session_controller. It’s worth having a glance at them and you’d likely want to include something similar in your project.

Thanks for reading and I hope you enjoyed the post!



Comment on this post!