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:
- A specification for composable modules between web applications
- 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:
- Plug.RequestId - generates a unique request id for each request
- Plug.Head - converts HEAD requests to GET requests
- Plug.Logger - logs requests
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
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.
def introspect(conn, _options) do
IO.puts """
Verb: #{inspect(conn.method)}
Host: #{inspect(conn.host)}
Headers: #{inspect(conn.req_headers)}
"""
conn
end
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:
-
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. -
call/2
which is nothing but a function plug and takes exactly the same two parameters -conn
andoptions
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
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:
-
init/1
function takes options, however does nothing with it. -
call/2
function takes conn and options as input - Private function
get_brearer_auth_token/1
takesconn
as input and tries findingauth_token
. - From
auth_token
we try finding an account. If we find the account, we add in ourconn
so that it is accessible to downstream plugs and controller functions. - In case we don’t find
auth_token
oraccount
we respond with401
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.
- Firewall - block traffic from barred IP addresses or clients.
- Last Seen - in messenger apps, last seen is maintained separately, using a plug you can update users' last seen value.
- Throttle - throttle requests depending upon limits set by you or pricing plans.
- Circuit Breaker - in case your downstream backend systems are facing trouble, you can decide to prevent traffic from creating more trouble.
Top comments (0)