For Stackd 2 we’re using Phoenix to build our API. This is my first time using Phoenix, and I love how you can use plugs and pattern matching to perform authentication and authorization. Here’s a look at how it works…

Authentication

The first thing we want to do is figure out who is making the request. Our API uses OAuth2, so authentication is done by including an OAuth token in the Authorization header. Our RequireToken plug checks the Authorization header for an OAuth token and if successful it will assign(conn, :token, token). If it’s not successful, it will halt the request and respond with an OAuth error.

defmodule Stackd.RequireToken do
  @behaviour Plug
  @auth_scheme "Bearer"

  use Plug.Builder
  import Phoenix.Controller, only: [render: 3]

  alias Stackd.{Repo, Token, ErrorView, Crypto.Pbkdf2}

  plug :require_auth_header
  plug :require_bearer_auth
  plug :assign_creds
  plug :assign_token
  plug :check_if_token_expired

  defp require_auth_header(conn, _) do
    if get_req_header(conn, "authorization") == [] do
      conn |> respond_with_error("invalid_token.json")
    else
      conn
    end
  end

  defp require_bearer_auth(conn, _) do
    if bearer_auth?(conn) do
      conn
    else
      conn |> respond_with_error("invalid_token.json")
    end
  end

  defp assign_creds(conn, _) do
    case String.split(bearer_auth_creds(conn), ".") do
      [id, token] -> conn |> assign(:creds, %{id: id, token: token})
      _           -> conn |> respond_with_error("invalid_token.json")
    end
  end

  defp assign_token(conn, _) do
    token = Repo.get(Token, conn.assigns.creds.id)
    access_secret = conn.assigns.creds.token

    if token && Pbkdf2.check(access_secret, token.access_secret_hash) do
      conn |> assign(:token, token)
    else
      conn |> respond_with_error("invalid_token.json")
    end
  end

  defp check_if_token_expired(conn, _) do
    if Token.expired?(conn.assigns.token) do
      conn |> respond_with_error("expired_token.json")
    else
      conn
    end
  end

  defp bearer_auth?(conn) do
    conn
    |> get_authorization_header
    |> String.starts_with?(@auth_scheme <> " ")
  end

  defp bearer_auth_creds(conn) do
    conn
    |> get_authorization_header
    |> String.slice(String.length(@auth_scheme) + 1..-1)
  end

  defp get_authorization_header(conn) do
    conn
    |> get_req_header("authorization")
    |> List.first
  end

  defp respond_with_error(conn, error) do
    conn
    |> put_resp_header("www-authenticate", "Basic realm=\"api.stackd.com\"")
    |> put_status(:unauthorized)
    |> render(ErrorView, error)
    |> halt
  end
end

Authorization

Our API has two different kinds of tokens:

  • Service tokens – used to access the API on behalf of internal services
  • User tokens – used to access the API on behalf of a user

With that in mind, let’s take a look at how our GET /users/:user_id/emails endpoint works. We want to make it so service tokens can list any user’s emails, but user tokens can only list their own emails.

First, we include the RequireToken plug. Then we use Elixir pattern matching to accomplish authorization. We have 3 different index/2 methods our request can match on:

  1. Service token => show the emails
  2. User token with the same user_id as the URL => show the emails
  3. Any other token => 404 not found
defmodule Stackd.User.EmailController do
  use Stackd.Web, :controller

  import Ecto.Query
  alias Plug.Conn
  alias Stackd.{RequireToken, Email, EmailView, ErrorView}

  plug RequireToken

  def index(conn = %Conn{assigns: %{token: %{service_id: service_id}}}, params)
  when is_binary(service_id), do: _index(conn, params)

  def index(conn = %Conn{assigns: %{token: %{user_id: user_id}}},
            params = %{"user_id" => user_id})
  when is_binary(user_id), do: _index(conn, params)

  def index(conn, _params) do
    conn
    |> put_status(:not_found)
    |> render(ErrorView, "not_found.json")
  end

  defp _index(conn, %{"user_id" => user_id}) do
    emails = Repo.all(Email |> where(user_id: ^user_id) |> order_by(desc: :inserted_at))
    conn
    |> put_status(:ok)
    |> render(EmailView, "index.json", emails: emails)
  end
end

Want to learn more about Stackd? Check out Origin to Pointer, our blog and podcast on tech, bootstrapping a business, product design, development, and whatever else is on our minds.