Plug authentication using remote endpoint

Hi everyone,

I have an Elixir app which uses Plug to serve a few REST HTTP API endpoints.

I need to implement the auth part but I really have no idea how to do it.

Here’s how the authentication works in the current Node.js REST server:

  1. Client sends request to Express.js server (in this case, Plug)
  2. Client provides ‘Authentication’ header with value of ‘Token auth_token’
  3. Server makes a HTTP request to a certain forum API endpoint with the Authentication header provided by the client
  4. If the forum log in request is done successfully, the Express.js request is authorized. Otherwise, a 401 error is sent.

The ‘forum endpoint’ is actually a Django app using TokenAuthentication.

What I currently have

I have a simple Plug REST API server which works fine. I also have a placeholder library for the Plug authentication:

defmodule ForumAuth do
  import Plug.Conn

  def init(opts), do: opts

  def authenticated?(conn) do
    true
  end

  def call(conn, _opts) do
    if authenticated?(conn) do
      conn
    else
      conn
      |> send_resp(401, "Not Authorized")
      |> halt
    end
  end
end

Any particular reason you are using Plug instead of Phoenix? There would be no notable difference for your use case as long as you uniquely serve an api on the same Plug app, as otherwise you might need Phoenix’ pipelines to run specific plugs on a specific scope - thus being able to serve anything else without interfering.

On to your question, there surely are many ways of doing this but you pretty much always end up with the need of creating custom plug-ables. How I do this is, I have a module with useful functions that can be plugged to perform specific actions - as this way you have things more compact as opposed to having a module for each plug you might need.

defmodule Something.Plugs do
	import Ecto.Query
	alias Something.{Repo, User}

	def fetch(conn, params) do
		id = conn.params["id"] # /api/posts/:id
		schema = params[:schema] # Post
		...
		cond do
			post ->
				conn
				|> assign(:post, post) # :post would instead be based off the schema's module name
			true ->
				error :not_found
		end
	end
	
	def authorize(conn, params) do
		cond do
			user = token_valid?(conn) ->
				route = conn.private[:phoenix_controller]
				action = get_action(conn)
				cond do
				  apply route, :can?, [user, action, conn.assigns] -> # Route.can?
					conn
					|> assign(:user, user)
				  true ->
					error :forbidden
				end
			  true ->
				error :unauthorized
			end
		end
	end
	
	defp get_action(conn) do
		conn.private[:phoenix_action]
	end
	
	defp token_valid?(conn) do
		conn
		|> get_token
		|> get_user
	end
	
	defp get_token(conn) do
		conn
		|> get_req_header("authorization")
		|> List.first
	end
	
	defp get_user(token) do
		cond do
			token ->
				User
				|> where(token: ^token)
				|> Repo.one
			true ->
				false
		end
	end

	defexception plug_status: 500

	defp error(atom) do
		raise __MODULE__, plug_status: atom |> Plug.Conn.Status.code
	end
end

This example may seem a bit off or complicated, but it’s really simple and may serve you as a reference. How this works is, you do plug :authorize and this would check whether a request has an Authorization header with a valid token and fetch whichever user it’s linked to - this obviously may differ depending on how you do tokens and store your data. Once token_valid? returns, if the value is false (meaning there’s no user for the input token or there’s no token at all) it raises an :unauthorized error, and if the value is indeed an user it grabs the current action (:get, :post, …) and calls a can? function, which altogether follows a Canada-like behaviour, on your current route that allows you to decide whether a specific user is allowed or not to perform an action on a fetched resource. This function is provided with conn.assigns, which contains all the resources that the :fetch plug would have stored.

Say the following was your route, which for reference may be configured like on this example.

defmodule Something.Route do
	use Phoenix.Controller

	plug :fetch, schema: Post # query a Post with the given id param
	plug :authorize
	
	def show(conn, params) do
		# we are authorized!
		# post = conn.assigns[:post]
	end
	
	def can?(user, action, resources) do
		# action = :get
		post = resources[:post]
		post.owner == user.id
	end
end

Concluding, keep in mind Repo is an Ecto repository and both User and Post are Ecto schemas. I would also need to mention that what error :unauthorized does is raising an error which then Phoenix catches and stops going further down the pipeline. There are many ways of doing this, but I found this to be more intuitive - you may find an explanation down here.

Hope you at least get a good idea on how to handle this kind of functionality! It was a bit hard to keep this short and compact, and so I will be expecting questions in case there’s any. I have thought in the past about publishing a set of useful plugs like these just so beginners have an example to work off, and might be releasing it soon.

1 Like