How to debug or check invalid CSRF token

I am implementing a session based authentication for a remix js framework (ssr framework) and it works initially not until I add csrf protection which results to invalid CSRF token.

I have this router

  pipeline :api do
    plug :accepts, ["json"]
    # plug :fetch_session
    plug MonoWeb.Plugs.TestPlug
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/api", MonoWeb do
    pipe_through :api

    get "/csrf_token", CSRFTokenController, :index
    post "/users/log_in", UserSessionController, :create
  end

CSRFTokenController

 def index(conn, _params) do
    conn
    |> put_resp_cookie("_csrf_token", get_csrf_token())
    |> send_resp(204, "")
  end

The auth flow with CSRF protection is like this

  1. call /csrf_token endpoint to set the csrf token in the cookie
  2. frontend parsed the response cookie
  3. get the csrf token from parsed cookie and send it as part of request header for call to /users/log_in
 const csrfResponse = await fetch("http://localhost:4000/api/csrf_token");
 const csrfCookies = await getCookiesFromResponse(csrfResponse);

  const response = await fetch("http://localhost:4000/api/users/log_in", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-csrf-token": csrfCookies["_csrf_token"],
    },
    body: JSON.stringify({
      user: {
        email: "test@test.com",
        password: "tester123456",
      },
    }),
    credentials: "include",
  });

I expect this one to work since I log the whole conn struct in a custom plug right before the :protect_from_forgery plug and the X-CSRF-TOKEN exists there.

%Plug.Conn{
  ...
  private: %{
    MonoWeb.Router => [],
    :before_send => [#Function<0.84243074/1 in Plug.Session.before_send/2>,
     #Function<0.11807388/1 in Plug.Telemetry.call/2>],
    :phoenix_endpoint => MonoWeb.Endpoint,
    :phoenix_format => "json",
    :phoenix_request_logger => {"request_logger", "request_logger"},
    :phoenix_router => MonoWeb.Router,
    :plug_session => %{},
    :plug_session_fetch => :done
  },
  ...
  req_headers: [
    {"accept", "*/*"},
    {"connection", "close"},
    {"content-length", "60"},
    {"content-type", "application/json"},
    {"host", "localhost:4000"},
    {"user-agent", "node-fetch"},
    {"x-csrf-token", "CVwCI2A9PGEtLB0ZHEV2Lh0bLC8JCXsry5HR1nt3UnYHK0Ok-JNcjqKe"}
  ],
  ...
 
}

but go this error instead.

[info] Sent 403 in 10ms
[debug] ** (Plug.CSRFProtection.InvalidCSRFTokenError) invalid CSRF (Cross Site Request Forgery) token, please make sure that:

  * The session cookie is being sent and session is loaded
  * The request include a valid '_csrf_token' param or 'x-csrf-token' header
    (plug 1.14.2) lib/plug/csrf_protection.ex:316: Plug.CSRFProtection.call/2
    (mono 0.1.0) MonoWeb.Router.api/2
    (mono 0.1.0) lib/mono_web/router.ex:1: MonoWeb.Router.__pipe_through0__/1
    (phoenix 1.7.6) lib/phoenix/router.ex:421: Phoenix.Router.__call__/5
    (mono 0.1.0) lib/mono_web/endpoint.ex:1: MonoWeb.Endpoint.plug_builder_call/2
    (mono 0.1.0) lib/plug/debugger.ex:136: MonoWeb.Endpoint."call (overridable 3)"/2
    (mono 0.1.0) lib/mono_web/endpoint.ex:1: MonoWeb.Endpoint.call/2
    (phoenix 1.7.6) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
    (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
    (cowboy 2.10.0) /home/hei/recode/mono/server/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.10.0) /home/hei/recode/mono/server/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.10.0) /home/hei/recode/mono/server/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 4.1.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Hmm … Have I missed something or you call plain fetch without CSRF token to get CSRF token and this ends up with a invalid token error (in api scope) …

yes I call plain fetch to get CSRF token without CSRF token since there are no CSRF token in the beginning to use and passed in the request header.

Checking the log, I got a success response for getting the CSRF token which I believe wouldn’t cause this issue.

Tried creating new pipeline just for csrf token and it still doesn’t work.

 pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
    plug MonoWeb.Plugs.TestPlug
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :no_csrf do
    plug :accepts, ["json"]
    plug :fetch_session
  end

  scope "/", MonoWeb do
    pipe_through :no_csrf
    get "/csrf_token", CSRFTokenController, :index
  end

  scope "/api", MonoWeb do
    pipe_through :api

    post "/users/log_in", UserSessionController, :create
  end

Frontend api call

const csrfResponse = await fetch("http://localhost:4000/csrf_token");
const csrfCookies = await getCookiesFromResponse(csrfResponse);
 
const response = await fetch("http://localhost:4000/api/users/log_in", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-TOKEN": csrfCookies["_csrf_token"]
    },
    body: JSON.stringify({
      user: {
        email: "test@test.com",
        password: "tester123456",
      },
    }),
    credentials: "include",
  });

I don’t remember how csrf_token works inside. If it’s not the API call mentioned then firstly I would check if below scenario is not true:

  1. Page loads with some csrf_token that’s saved somewhere (session?)
  2. API call fetches new csrf_token (not the current one in session)
  3. Next API call with csrf_token check gives error because csrf_token does not match

Again I’m not sure. The only error I had in past with csrf_token was some kind of typo or so in regular form. It was years ago, but if remember correctly you can store csrf_token in different way …

Try something like it in your layout:

…
<script>
  var csrf_token = "<%= get_csrf_token(@conn) %>";
</script>
…

Keep in mind I’m writing it from memory, but there should be a function (I guess imported from Plug.Conn or so) for fetching said token from session.