Simple authentication in Elixir with Plug
This past week I began building an API in Elixir. Most of the guides and posts I read were focused on the Phoenix framework, and it was a little difficult to find an example on how to perform authentication using Plug.
Here is what I used:
defmodule Api.Authentication do
import Plug.Conn
def init(opts), do: opts
def authenticated?(conn) do
# Implement authentication logic here, e.g. check auth headers,
# session creds, etc.
false
end
def call(conn, _opts) do
if authenticated?(conn) do
conn
else
conn
|> send_resp(401, "")
|> halt
end
end
end
In the call
method we can check to see if a request is authenticated. If
the request passes our authentication test we send the request downstream to
the next plug in our stack for further processing. If the request does not
pass our authentication test we immediately terminate the request using
halt
and respond with an HTTP 401 status.
Once we have the plug we can add it to our router using the plug
method.
alias Api.Authentication
defmodule Api do
use Plug.Router
plug Authentication # run authentication
plug :match
plug :dispatch
match _ do
conn
|> put_resp_content_type("text/plain")
|> send_resp(404, "")
end
end
And that is it!
$ curl -I localhost:8085/api
HTTP/1.1 401 Unauthorized
server: Cowboy
date: Fri, 22 Jul 2016 15:32:10 GMT
content-length: 0
cache-control: max-age=0, private, must-revalidate
One gotcha you might encounter is the ordering of plugs. Order does matter
when we are creating a router. In this case, plug Authentication
should
come before the other plugs :match
and :dispatch
. If we don't order it
correctly the server will proccess the authentication check after other route
handlers are called.
For example, if we put our authentication plug at the end, like so, the router will try to send a response twice and our process will terminate.
defmodule Api do
use Plug.Router
plug :match
plug :dispatch
plug Authentication # authentication at the end
The server responds with a 404 and an uncaught exception appears in the server log.
08:15:07.493 [error] #PID<0.262.0> running Router terminated
Server: localhost:8085 (http)
Request: GET /api
** (exit) an exception was raised:
** (Plug.Conn.AlreadySentError) the response was already sent
(plug) lib/plug/conn.ex:458: Plug.Conn.resp/3
(plug) lib/plug/conn.ex:445: Plug.Conn.send_resp/3
(office_mate) lib/office_mate/api_authentication.ex:19: Api.Authentication.call/2
(office_mate) lib/office_mate/api.ex:3: Api.plug_builder_call/2
(plug) lib/plug/router/utils.ex:74: Plug.Router.Utils.forward/4
(office_mate) lib/office_mate/router.ex:1: Router.plug_builder_call/2
(plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Handler.upgrade/4
(cowboy) src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4