DEV Community

Abhinav Saxena
Abhinav Saxena

Posted on • Originally published at abhinav.co

Elixir Plugs

Of late, I have been writing some Elixir code to build an API layer for Soopr, a SaaS tool I am working on. This system needed a simplistic authentication mechanism and I ended up writing a custom Plug for it. This post explains what are plugs and how you can write one yourself.

What is a Plug?

In Elixir world, Plug is a bit similar to Rack in Ruby. Official documentation describes Plug as:

  1. A specification for composable modules between web applications
  2. An abstraction layer for connection adapters for different web servers in the Erlang VM

Plugs can be chained together and they can be very powerful as well as can add simplicity and readability to your code.

If I were to describe Plug, plugs are a mechanism through which either you can transform your request/response or you can decide to filter requests that reach to your controller.

In Plug/Phoenix world, %Plug.Conn{} in the connection struct that contains both request & response paramters and is typically called conn.

Transformation

It essentially means modifying conn (technically, creating a copy of original conn) and adding more details to it. These plugs also need to return modified conn struct so that plugs can be chained together. Some examples of plugs are:

Filter

There are cases when you want to stop HTTP requests if they don’t meet your criteria. Example - they come from a blocked IP or more common example - don’t have required authentication details. In these cases you use such plugs. Plug.BasicAuth is one such plug, it provides Basic HTTP authentication.


How are plugs chained?

In Phoenix world, plugs are generally added to your endpoint.ex or router.ex modules.

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  import Plug.BasicAuth

  pipeline :api do
    plug :accepts, ["json"] 
    plug :basic_auth,
      username: System.fetch_env!("AUTH_USER"),
      password: System.fetch_env!("AUTH_KEY")
  end

  scope "/api/v1/", MyAppWeb do
    pipe_through :api
    get "/posts", PostController, :index
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example - our api pipeline would accept only json requests and require basic authentication. This api pipeline is then used to define a GET /api/v1/posts route.


How to build your own Plug?

There are two ways in which you can write your own Plug - Function or Module.

1. Function Plugs

Function plugs are generally used if your plug is simple enough and doesn’t have any costly initialisation requirement. To write a function plug, simply define a function which takes two inputs - conn and options.

Example:

def introspect(conn, _options) do
  IO.puts """
  Verb: #{inspect(conn.method)}
  Host: #{inspect(conn.host)}
  Headers: #{inspect(conn.req_headers)}
  """
  conn
end
Enter fullscreen mode Exit fullscreen mode

2. Module Plugs

Module plugs are useful when you have a bit heavy initialisation process or you need auxilary functions to keep your code readable. For a Module Plug, you need you to define following two functions inside an elixir module:

  1. init/1 where you can do the initialisation bit. It takes options as input, something that you can pass when using it in router or endpoint file.
  2. call/2 which is nothing but a function plug and takes exactly the same two parameters - conn and options
defmodule MyAppWeb.BearerAuth do

  import Plug.Conn
  alias MyApp.Account

  def init(options) do
    options
  end

  def call(conn, _options) do
    case get_bearer_auth_token(conn) do
      nil ->
        conn |> unauthorized()
      :error ->
        conn |> unauthorized()
      auth_token ->
        account =
          Account.get_from_token(auth_token)
        if account do
          assign(conn, :current_account, account)
        else
          conn |> unauthorized()
        end
    end
  end

  defp get_bearer_auth_token(conn) do
    with ["Bearer " <> auth_token] <- get_req_header(conn, "authorization") do
      auth_token
    else
      _ -> :error
    end
  end

  defp unauthorized(conn) do
    conn
    |> resp(401, "Unauthorized")
    |> halt()
  end
end
Enter fullscreen mode Exit fullscreen mode

This is taken from an actual plug which I wrote for Soopr (I have just changed the names). Though I didn’t have any heavy initialisation requirements, I decided to use module way of writing so that I can define a couple of private helper functions. In this example:

  1. init/1 function takes options, however does nothing with it.
  2. call/2 function takes conn and options as input
  3. Private function get_brearer_auth_token/1 takes conn as input and tries finding auth_token.
  4. From auth_token we try finding an account. If we find the account, we add in our conn so that it is accessible to downstream plugs and controller functions.
  5. In case we don’t find auth_token or account we respond with 401 and halt the request, unauthorized/1 function takes care of that.

Interesting Use cases

Here are a few interesting problems which you can possibly solve using your own custom plugs.

  1. Firewall - block traffic from barred IP addresses or clients.
  2. Last Seen - in messenger apps, last seen is maintained separately, using a plug you can update users' last seen value.
  3. Throttle - throttle requests depending upon limits set by you or pricing plans.
  4. Circuit Breaker - in case your downstream backend systems are facing trouble, you can decide to prevent traffic from creating more trouble.

Top comments (0)