Adding a plug to check for authentication

Hi,

I am starting my journey to learn Phoenix. I was trying to add a function plug to redirect a user to the sign in page if they are not signed in (which is checked by checking if there is an authcookie present and if its value is valid). The plug function that I added (code included below) does the job correctly. However, it throws a big error in my terminal that I do not understand. Could someone please help me understand the error, and how do I fix it?
In general, I see that error output is very large. Are there also some pointers on where to look for the error in the big error dump.

Thank you very much.

Function Plug to check signed in status… added in router.ex

def check_signedin(conn, _opts) do
    authcookie = conn
                  |> Map.get(:req_cookies)
                  |> Map.get("authcookie")
    if is_nil(authcookie) do
      IO.puts("Not signed in...redirecting!")
      conn
        |> redirect(to: "/auth/signin")
        |> halt()
    end
    conn
end

Relevant routes in router.ex

scope "/", PhxappWeb do
  pipe_through [:browser, :check_signedin]

  get "/", PageController, :index

end

scope "/auth", PhxappWeb do
  pipe_through [:browser]

  ## Auth routes
  get "/signin", AuthController, :signin
end

Error that I see in the terminal - Functionality works fine, but this error is thrown

Not signed in...redirecting!

[debug] Processing with PhxappWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser, :check_signedin]
[info] Sent 302 in 563µs
[info] Sent 200 in 2ms
[error] Ranch listener PhxappWeb.Endpoint.HTTP had connection process started
with :cowboy_clear:start_link/4 at #PID<0.577.0> exit with reason:
{:function_clause, [{:cowboy_http, :commands, [{:state, #PID<0.459.0>,
PhxappWeb.Endpoint.HTTP, #Port<0.22>, :ranch_tcp, :undefined, %{env:
%{dispatch: [{:_, [], [{:_, [], Phoenix.Endpoint.Cowboy2Handler,
{PhxappWeb.Endpoint, []}}]}]}, stream_handlers: [:cowboy_telemetry_h,
:cowboy_stream_h]}, "", %{}, {{127, 0, 0, 1}, 58027}, {{127, 0, 0, 1}, 4000},
:undefined, #Reference<0.4072028587.2291138566.197604>, true, 2,
{:ps_request_line, 0}, 65535, 1, :done, 1000, [{:stream, 1,
{:cowboy_telemetry_h, {:state, {:cowboy_stream_h, {:state, :undefined,
PhxappWeb.Endpoint.HTTP, #PID<0.578.0>, :undefined, :undefined, :undefined,
:undefined, 0, :nofin, "", 0, ...}}, #Function<0.118987493/1 in
:cowboy_telemetry_h."-fun.metrics_callback/1-">, :undefined, %{body_length: 0,
cert: :undefined, has_body: false, headers: %{"accept" =>
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding" => "gzip, deflate, br", "accept-language" =>
"en-US,en;q=0.9", "connection" => "keep-alive", "cookie" =>
"_phxapp_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOXVna0FLX0U3OHRmNHZjcE0xY3dVVm00.Faa8gdSg7P43PVUgba_2q-DC6OULbiAYNch2LFdIr_8",
"host" => "localhost:4000", "sec-fetch-dest" => "document", ...}, host:
"localhost", method: "GET", path: "/", peer: {{127, 0, ...}, 58027}, pid:
#PID<0.577.0>, port: 4000, qs: "", ...}, "200 OK", %{"cache-control" =>
"max-age=0, private, must-revalidate", "content-length" => "2351",
"content-type" => "text/html; charset=utf-8", "cross-origin-window-policy" =>
"deny", "date" => "Sat, 25 Dec 2021 00:03:45 GMT", "server" => "Cowboy",
"set-cookie" =>
["_phxapp_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYMEh0MTJOOVdwMEpycTV2aHhzY19oX2p3.FmbAyd1z8ZyRGGbxdu9grQEA1l30blIXdZXoxlB2a7o;
path=/; HttpOnly"], "x-content-type-options" => "nosniff", "x-download-options"
=> "noopen", ...}, PhxappWeb.Endpoint.HTTP, -576460559211169000, :undefined,
:undefined, :undefined, -576460558919931000, -576460558919931000,
%{#PID<0.578.0> => %{...}}, [], ...}}, "GET", :"HTTP/1.1", :undefined,
:undefined, 0, []}], [{:child, #PID<0.578.0>, 1, 5000, :undefined}]}, 1,
[{:response, "200 OK", %{"cache-control" => "max-age=0, private,
must-revalidate", "content-length" => "2351", "content-type" => "text/html;
charset=utf-8", "cross-origin-window-policy" => "deny", "date" => "Sat, 25 Dec
2021 00:03:45 GMT", "server" => "Cowboy", "set-cookie" =>
["_phxapp_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYMEh0MTJOOVdwMEpycTV2aHhzY19oX2p3.FmbAyd1z8ZyRGGbxdu9grQEA1l30blIXdZXoxlB2a7o;
path=/; HttpOnly"], "x-content-type-options" => "nosniff", "x-download-options"
=> "noopen", "x-frame-options" => "SAMEORIGIN",
"x-permitted-cross-domain-policies" => "none", "x-request-id" =>
"FsPWlThyjRhtJ3cAAAck", "x-xss-protection" => "1; mode=block"}, ["<!DOCTYPE
html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta
http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta
name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta
content=\"QitDAFslQQFBQDMrA1gRAA84Ji4FPFw0rc71ikxV1pyYrmghwKEqmc6C\"
name=\"csrf-token\">\n<title data-suffix=\" · Phoenix Framework\">Phxapp ·
Phoenix Framework</title>\n    <link phx-track-static rel=\"stylesheet\"
href=\"/assets/app.css\">\n    <script defer phx-track-static
type=\"text/javascript\" src=\"/assets/app.js\"></script>\n  </head>\n
<body>\n    <header>\n      <section class=\"container\">\n        <nav>\n
<ul>\n            <li><a href=\"https://hexdocs.pm/phoenix/overview.html\">Get
Started</a></li>\n\n              <li><a
href=\"/dashboard\">LiveDashboard</a></li>\n\n          </ul>\n        </nav>\n
<a href=\"https://phoenixframework.org/\" class=\"phx-logo\">\n          <img
src=\"/images/phoenix.png\" alt=\"Phoenix Framework Logo\">\n        </a>\n
</section>\n    </header>\n<main class=\"container\">\n  <p class=\"alert
alert-info\" role=\"alert\"></p>\n  <p class=\"alert alert-danger\"
role=\"alert\"></p>\n<section class=\"phx-hero\">\n  <h1>Welcome to
Mystudybook!</h1>\n  <p>Peace of mind from prototype to
production</p>\n</section>\n\n<section class=\"row\">\n  <article
class=\"column\">\n    <h2>Resources</h2>\n    <ul>\n      <li>\n        <a
href=\"https://hexdocs.pm/phoenix/overview.html\">Guides &amp; Docs</a>\n
</li>\n      <li>\n        <a
href=\"https://github.com/phoenixframework/phoenix\">Source</a>\n      </li>\n
<li>\n        <a
href=\"https://github.com/phoenixframework/phoenix/blob/v1.6/CHANGELOG.md\">v1.6
Changelog</a>\n      </li>\n    </ul>\n  </article>\n  <article
class=\"column\">\n    <h2>Help</h2>\n    <ul>\n      <li>\n        <a
href=\"https://elixirforum.com/c/phoenix-forum\">Forum</a>\n      </li>\n
<li>\n        <a href=\"https://web.libera.chat/#elixir\">#elixir on Libera
Chat (IRC)</a>\n      </li>\n      <li>\n        <a
href=\"https://twitter.com/elixirphoenix\">Twitter @elixirphoenix</a>\n
</li>\n      <li>\n        <a
href=\"https://elixir-slackin.herokuapp.com/\">Elixir on Slack</a>\n
</li>\n      <li>\n        <a href=\"https://discord.gg/elixir\">Elixir on
Discord</a>\n      </li>\n    </ul>\n  </article>\n</section>\n\n</main>\n\n
", "<iframe hidden height=\"0\" width=\"0\"
src=\"/phoenix/live_reload/frame\"></iframe>", "</body>", "\n</html>\n"]}]],
[file:
'/Users/myusername/experimental_apps/phxapp/deps/cowboy/src/cowboy_http.erl',
line: 957]}, {:cowboy_http, :loop, 1, [file:
'/Users/myusername/experimental_apps/phxapp/deps/cowboy/src/cowboy_http.erl',
line: 257]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line:
226]}]}

[info] GET /auth/signin
Verb: "GET"
Host: "localhost"
Headers: [{"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"}, {"accept-encoding", "gzip, deflate, br"}, {"accept-language", "en-US,en;q=0.9"}, {"connection", "keep-alive"}, {"cookie", "_phxapp_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOXVna0FLX0U3OHRmNHZjcE0xY3dVVm00.Faa8gdSg7P43PVUgba_2q-DC6OULbiAYNch2LFdIr_8"}, {"host", "localhost:4000"}, {"sec-fetch-dest", "document"}, {"sec-fetch-mode", "navigate"}, {"sec-fetch-site", "none"}, {"sec-fetch-user", "?1"}, {"sec-gpc", "1"}, {"upgrade-insecure-requests", "1"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"}]

This is actually slightly incorrect,

def check_signedin(conn, _opts) do
    authcookie = conn
                  |> Map.get(:req_cookies)
                  |> Map.get("authcookie")
    if is_nil(authcookie) do
      IO.puts("Not signed in...redirecting!")
      conn
        |> redirect(to: "/auth/signin")
        |> halt()
    end
    conn # <= this is the original conn sent in to the function
end

Effectively you have this:

def check_signedin(conn, _opts) do
    authcookie = conn
                  |> Map.get(:req_cookies)
                  |> Map.get("authcookie")
    new_conn =
      if is_nil(authcookie) do
        IO.puts("Not signed in...redirecting!")
        conn
          |> redirect(to: "/auth/signin")
          |> halt()
      end

    conn
end

What you need is:

def check_signedin(conn, _opts) do
    authcookie = conn
                  |> Map.get(:req_cookies)
                  |> Map.get("authcookie")
    case authcookie do
      nil -> 
        IO.puts("Not signed in...redirecting!")
        conn
          |> redirect(to: "/auth/signin")
          |> halt()
      _ -> conn
    end
end

Which is effectively:

def check_signedin(conn, _opts) do
    authcookie = conn
                  |> Map.get(:req_cookies)
                  |> Map.get("authcookie")
    new_conn =
      case authcookie do
        nil -> 
          IO.puts("Not signed in...redirecting!")
          conn
            |> redirect(to: "/auth/signin")
            |> halt()
        _ -> conn
      end
    new_conn
end

See cowboy_tls:start_link/4 exits with reason :function_clause · Issue #1421 · ninenines/cowboy · GitHub

You can see in your log:

Not signed in...redirecting! (<== this is "GET /")

[debug] Processing with PhxappWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser, :check_signedin]
[info] Sent 302 in 563µs
[info] Sent 200 in 2ms
[error]...
[info] GET /auth/signin (<=== new request)

We send the 302 redirect, then send a 200, after sending a 302 down that conn, so the 200 causes the crash. Though you’re not returning the correct conn (or maybe copy-paste error), perhaps the redirect is send immediately on call by Plug.

3 Likes

Thank you so much! I did not know that the redirect() -> halt() returns a modified connection which must be returned.

Thanks again!

1 Like

It’s a shame the error is so gnarly. I think because 1) cowboy is written in erlang and 2) it’s a “low level” crash, elixir isn’t catching the error and presenting it nicely which I believe it does when it can.

By the way check out mix phx.gen.auth, though experimenting yourself is a worthy pursuit too. (Maybe use gen.auth in your release app.)

1 Like