Multiple Routers with Plug and Forward

Breno Moura
wearejaya
Published in
3 min readDec 19, 2019

Recently I needed to create a new path for our Elixir app. This endpoint leads to a page that has some delicate data like how many users are using the app, the main organizations, and workspace and how many repositories those organizations have associated with the app. So for this specific path, we needed some authentication.

FIRST THINGS FIRST

Currently, on our Elixir app, all the web layer is made using Plug — Yes, we are planning to move this layer to Pheonix, but not yet — The reason for this is very simple, just a few things are needed to be "on web" so use Plugs is fair enough for two main reasons:

  1. Plug is really very useful, powerful and easy to use;
  2. At this moment, Pheonix is "too much" for our needs

THE PROBLEM

As I told before, we have a couple of endpoints on our application that lead to statics pages, those pages are available for everybody and don't need any kind of authorization BUT we needed to create a new endpoint to show some internal data.

Those data are for internal use only and, first needed to be processed and then displayed using some charts, etc, etc. And of course, as every internal data, we can't leave this open for an unauthorized user, we need to add authentication for this specific endpoint and only authorized users can see this data.

To solve this situation first I created a new router called AuthenticatedRouter.ex because only the paths included on this router will need authentication— and then, after including some macros through use Plug.Router and set up two of the built-in Plugs: :match and :dispatch, I added the BasicAuth plug.

defmodule Eleminder.AuthenticatedRouter do
@moduledoc """
This router add authentication for /stats
"""
use Plug.Router
plug(:match)
plug(BasicAuth, use_config: {:eleminder, :basic_auth})
plug(:dispatch)
get("/stats", to: Eleminder.Plugs.Stats)
match(_, do: send_resp(conn, 404, "Resource not found"))
end

After creating the custom router for authenticated paths, now I just need to tell to my Router.ex: "Hey, for every request for my path /stats, please forward to my /statson my AuthenticatedRouter.ex"

defmodule Eleminder.Router do
use Plug.Router

plug(:match)
plug(:dispatch)
# ommited routes forward("/stats", to: Eleminder.AuthenticatedRouter) match(_, do: send_resp(conn, 404, "Resource not found"))
end

Let's check it.

➜ curl -i http://localhost:4001/stats
HTTP/1.1 401 Unauthorized
cache-control: max-age=0, private, must-revalidate
content-length: 16
content-type: text/plain; charset=utf-8
date: Thu, 19 Dec 2019 17:26:28 GMT
server: Cowboy
www-authenticate: Basic realm="Admin Area"
401 Unauthorized

Perfect! I was redirected to the router that needs authentication as I expected and as I'm not authorized a 401 Unauthorized was returned.

Now let's test sending the username and password for authentication.

➜ curl -i -u admin:123456 http://localhost:4001/stats
HTTP/1.1 404 Not Found
cache-control: max-age=0, private, must-revalidate
content-length: 18
date: Thu, 19 Dec 2019 17:27:32 GMT
server: Cowboy
Resource not found

Hmm... 🤔 Resource not found. But why?

WHAT REALLY HAPPENED HERE

Elixir and Plug did everything alright. First, it recognized my request, it was for a valid path, then authentication was done and then forwarded for another router as I asked.

What I didn't really realize here was theforwad command that keeps the path defined to be forward. So the correct endpoint actually is /stats/stats.

Let's test it.

curl -i -u admin:123456 http://localhost:4001/stats/stats
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 5699
date: Thu, 19 Dec 2019 17:55:17 GMT
server: Cowboy

Bingo! Works perfectly. Now what I just need to do is define a namespace for our paths that needs authentication and everything will be secure and easy to read.

So our AuthenticatedRouter.ex still the same and our Router.ex will be like

forward("/auth", to: Eleminder.AuthenticatedRouter)

Let’s test it.

➜ curl -i -u admin:123456 http://localhost:4001/auth/stats
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 5699
date: Thu, 19 Dec 2019 18:16:34 GMT
server: Cowboy

Perfect! Now every path that I add on my AuthenticatedRouter.ex will need authentication and the paths included on the Router.ex still available without need user and password.

So that's it for today. I hope this article helps you and if you have any suggestions or questions, please send a comment.

Links:

https://elixirschool.com/en/lessons/specifics/plug/
https://hexdocs.pm/plug/Plug.Router.html#forward/2
https://github.com/paulanthonywilson/basic_auth#how-to-use

--

--