Elixir authorization plugs

Similar to Ruby’s Rack, Plug is a general specification for composing modules between web applications and application servers. Here’s how we can use them to build authorized pipelines in your router.

Note that this post is not about whether you should do authorization at the router level. It’s likely you’ll do it as part of your business logic for the most part. But when it makes sense, you can use Plugs.

Authorization plug is a generic plug, but one that might need to stop the execution of the request. If we import Plug.Conn we can pipe to halt/1 to do exactly that. We should also render a custom error message.

I’ll demonstrate by checking claims from a Guardian token and compare them to fix hardcoded access rights.

defmodule DigiWeb.VerifyAdminPlug do
  import Plug.Conn

  def init(options), do: options

  # This is what's get called
  def call(%Plug.Conn{} = conn, opts) do
    verify_access!(conn, opts)
  end

  defp verify_access!(conn, opts) do
    # This is a path like "/admin/path1"
    authorized_path = conn.request_path

    # We fetch the authorization rights from claims
    rights =
      if Map.has_key?(conn.private, :guardian_default_claims) do
        conn.private.guardian_default_claims["rights"]
      else
        []
      end

    # A simple way of checking a path against a particular right,
    # could be a more sophisticated check
    required_rights = required_action_rights[authorized_path]

    if required_rights do
      case has_rights(rights, required_rights) do
        false ->
          conn
          |> auth_error()
          |> halt()

        true ->
          conn
      end
    else
      conn
      |> auth_error()
      |> halt()
    end
  end

  defp has_rights([], _required), do: false

  defp has_rights(rights, required) do
    MapSet.subset?(MapSet.new(rights), MapSet.new(required))
  end

  defp auth_error(conn) do
    body = Poison.encode!(%{message: "Unauthorized"})
    send_resp(conn, 401, body)
  end

  def required_action_rights do
    %{
      "/admin/path1" => ["right1"],
      "/admin/path2" => ["right2"]
    }
  end
end

There you have it, a simple way to authorize access. Great for pages that are protected entirely.

To make a route use your plug, plug it in a pipeline:

# router.ex
  ..
  pipeline :authorized_admin do
    plug Guardian.Plug.Pipeline,
      module: MyAppWeb.Public.Guardian,
      error_handler: MyAppWeb.Public.ErrorHandler

    plug DigiWeb.Public.GraphiQlAuthentication

    # Check admin's access rights
    plug MyApp.VerifyAdminPlug
  end
  ..

Because I wanted to use claims from a token issued by Guardian, I put it after the Guardian’s plug.

← IT'S OUT NOW

I wrote a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects.

More →