Using a plug for resource authorization, while still allowing for 404s

I’m writing an application that has a set of resources that require an authorization token to access. Instead of repeating logic in my routes (or worse, potentially forgetting it), I decided to write a plug that would handle this for me. All the routes that need to be authenticated have a :timer_id parameter in the route, which I then check against the user’s token to ensure they have access to this timer. Here’s a simplified version of my plug

defmodule MyApp.AuthPlug do
  def init(opts), do: opts

  def call(conn, opts) do
    timer_id = timer_id_from_request(conn)
    if Auth.token_valid_for_timer(conn, timer_id) do
      conn
    else
      send_resp(conn, 401, "")
      |> halt
    end
  end

  defp timer_id_from_request(conn) do
    case Map.get(conn.params, "timer_id") do
      nil ->
        raise "timer id param is not present in connection parameters. This is likely a programming error."

      timer_id ->
        timer_id
    end
  end
end

This allows me to ensure that all requests which pass through this plug are properly authenticated, and it’s not possible to accidentally make a route not authenticated by misspelling the route parameter. However, one flaw with this approach is 404 handlers. Now, the following router will throw a 500 on a non-matching route, rather than the 404. This is because when the request passes through the auth plug, it sees there’s no timer_id parameter on the wildcard route, and it bombs out.

defmodule MyApp.Router do
  use Plug.Router
  
  plug(:match)
  plug(MyApp.AuthPlug)
  plug(:dispatch)
  
  get "/timer/:timer_id" do
    # ...
  end 

  match _ do
    send_resp(conn, 404, "")
  end
end

The way I see it, I have 3 options

  1. Find some way to detect that I’m in this catch-all case in the plug, and skip the parameter check if so. I attempted to use Plug.Router.match_path/1 to get the matched path, but I’m not sure exactly how to parse this format to achieve what I want. Looking at its value, I see I get back /my/base/url/*glob/*_path on an unmatched route, but I don’t know if I can depend on this value being stable.
  2. Omit the defensive check. While this works, I do kind of like that I am prevented from creating an unauthenticated endpoint in an authenticated module by accident.
  3. Give up on this plug idea entirely.

Is there some way to achieve what I want? Am I going about this in a totally backwards way?

1 Like

In my last project we just had some fairly minimal boilerplate that returns {:error, :not_found} or {:error, :not_allowed} and then had a Plug that set responses based on that.

At first I was cringing a bit about it but the project was not as huge and it can be manually managed and enforced in PR reviews, and it is also more explicit and skips magic macros that would make people blind to what’s going on. So it turned out to be a win.

1 Like

Tangential note - the HTTP 401 Unauthorized response code is specifically intended for resources that could be accessible if the right Authorization header is sent, and responses that use it MUST include a WWW-Authenticate challenge header. From RFC 7235

   The 401 (Unauthorized) status code indicates that the request has not
   been applied because it lacks valid authentication credentials for
   the target resource.  The server generating a 401 response MUST send
   a WWW-Authenticate header field (Section 4.1) containing at least one
   challenge applicable to the target resource.

Unless you intend for this meaning, prefer responding with 403 Forbidden or 404 Not Found.

Good eye; this is a greatly simplified example but thank you!

TIL about WWW-Authenticate though. I don’t think I’ve ever seen that from an API

Makes sense. Something to be said for not over-engineering :slight_smile:

Thanks!