Logical OR of two plugs in a pipeline in router.ex

Hi folks,
This is my first Phoenix/Web application .
My phoenix app uses an authentication scheme based on JWTs. I need to modify the authentication code base to support another authentication method which I’ll call “x-scheme”. For backward compatibility, I need to support the JWT method as well. Thinking out loud, I need a disjunction (logical or) of both schemes similar to the code snippet below:

 pipeline :authenticate do
     plug(:jwt_auth) or plug(:x_scheme_auth)                                                                                                                         
 end

What is the best way to address this in Elixir/Phoenix? Is this even a good idea? Any help is much appreciated.

:wave: @_Seyed

Maybe it could be another plug.

pipeline :authenticate do
  plug :authed
end

def authed(conn, opts) do
  case jwt_auth(conn, opts) do
    %Plug.Conn{halted: true} -> x_scheme_auth(conn, opts)
    conn -> conn
  end
end
3 Likes

Hi @ruslandoga,

Thanks for reaching out! I have not used :halted in implementing the jwt_auth/2method; could you please explain its usage? I did not understand its documentation.

I think I found a solution. Should I redirect my pipeline to a halt if it fails the first schema?

def  jwt_auth(conn, opts) do
..... #sign / decode / extract headers
..... 
case verification do 
   good_case -> 
      # extract / proceed
   error -> 
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(:unauthorized, '{ "detail": " authorization failed."} ')
      |> halt()
end

It’s a field that gets set by plug when you (or the underlying auth library) calls Plug.halt, which is happening when the code figures that its job of the plug is not done / has failed (I’d guess in the case of invalid or expired JWT auth token).

EDIT: oops, I posted literal seconds after you. I’d say yes, try that, it should work.

1 Like

Thanks a million @dimitarvp

Nice, the authed wrapper plug makes sense to me.

A small thing I noticed is that the variable name conn is reused such that there are two different Plug.Conn states both named conn.

To avoid any confusion down the line, I’d rename the matched conn in the case block to distinguish it from the parameter conn.

def authed(conn, opts) do
  case jwt_auth(conn, opts) do
    %Plug.Conn{halted: true} -> x_scheme_auth(conn, opts)
    authed_conn -> authed_conn
  end
end
1 Like

You can compose plugs at runtime with Plug — Plug v1.14.2.

2 Likes

Nice link but:

If any of the plugs halt, the remaining plugs are not invoked.

I read the above comments as OP wanting the opposite: run plugs until one succeeds.

1 Like

When halt/1 is called on a plug, it just means that the plug functions that were registered to be called further in the “pipeline” would not be called.

Mix.install [:plug]

defmodule Endpoint do
  use Plug.Builder

  def print(conn, opts) do
    IO.puts(Keyword.fetch!(opts, :message))
    conn
  end

  def auth(conn, opts) do
    if opts[:pass] do
      conn
    else
      conn |> Plug.Conn.resp(401, "not authed") |> halt()
    end
  end

  plug :print, message: "1"
  plug :auth, pass: false
  plug :print, message: "2"
end

Endpoint.call(%Plug.Conn{}, [])

prints 1

I assumed halt/1 was called in jwt_auth/2 and x_scheme_auth/2 when the authentication failed.

2 Likes

Hi @codeanpeace ,
Thanks a lot for the insight.