Invalid csrf error on phoenix 1.6.7 in (Plug.CSRFProtection.InvalidCSRFTokenError)

In development I noticed that when I update to the latest version of Phoenix (1.6.7), I now receive an invalid csrf error when attempting to login.

Deps Versions that work

  • phoenix 1.6.6
  • plug 1.13.4 / 1.13.6
  • phoenix_live_view 0.17.7 / 0.17.9
  • phoenix_pubsub 2.1.0 / 2.1.1

Error on phoenix 1.6.7

  * 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.13.6) lib/plug/csrf_protection.ex:316: Plug.CSRFProtection.call/2
    (metamorphic_web 0.1.0) MetamorphicWeb.Router.browser/2
    (metamorphic_web 0.1.0) lib/metamorphic_web/router.ex:1: MetamorphicWeb.Router.__pipe_through9__/1
    (phoenix 1.6.7) lib/phoenix/router.ex:347: Phoenix.Router.__call__/2
    (metamorphic_web 0.1.0) lib/metamorphic_web/endpoint.ex:1: MetamorphicWeb.Endpoint.plug_builder_call/2
    (metamorphic_web 0.1.0) lib/plug/debugger.ex:136: MetamorphicWeb.Endpoint."call (overridable 3)"/2
    (metamorphic_web 0.1.0) lib/metamorphic_web/endpoint.ex:1: MetamorphicWeb.Endpoint.call/2
    (phoenix 1.6.7) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy 2.9.0) /home/mark/github/eclogite/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /home/mark/github/eclogite/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.9.0) /home/mark/github/eclogite/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 3.17) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

Router
Below is router.ex file:

defmodule MetamorphicWeb.Router do
  use MetamorphicWeb, :router

  import Phoenix.LiveDashboard.Router
  import MetamorphicWeb.PersonAuth

  alias MetamorphicWeb.Plugs.{EnsurePrivilege, VerifyPortalPass, PlugAttack}

  if Application.get_env(:metamorphic_web, :http_headers) == :prod do
    pipeline :browser do
      plug PlugAttack
      plug :accepts, ["html"]
      plug :fetch_session
      plug :fetch_live_flash
      plug :protect_from_forgery

      plug :put_secure_browser_headers, %{
        "referrer-policy" => "no-referrer",
        "permissions-policy" => "interest-cohort=()",
        "strict-transport-security" => "max-age=31536000; includeSubDomains"
      }

      plug :fetch_current_person
    end

    pipeline :gpc do
      plug PlugAttack
      plug :accepts, ["json"]
    end
  else
    pipeline :browser do
      plug PlugAttack
      plug :accepts, ["html"]
      plug :fetch_session
      plug :fetch_live_flash
      plug :protect_from_forgery

      plug :put_secure_browser_headers, %{
        "referrer-policy" => "no-referrer",
        "permissions-policy" => "interest-cohort=()"
      }

      plug :fetch_current_person
    end

    pipeline :gpc do
      plug PlugAttack
      plug :accepts, ["json"]
    end
  end

  pipeline :api do
    plug PlugAttack
    plug :accepts, ["json"]
  end

  pipeline :marketing_layout do
    plug :put_root_layout, {MetamorphicWeb.LayoutView, :marketing}
  end

  pipeline :pre_release_layout do
    plug :put_root_layout, {MetamorphicWeb.LayoutView, :pre_release}
  end

  pipeline :register_layout do
    plug :put_root_layout, {MetamorphicWeb.LayoutView, :register}
  end

  pipeline :app_layout do
    plug :put_root_layout, {MetamorphicWeb.LayoutView, :root}
  end

  pipeline :admin do
    plug EnsurePrivilege, :admin
  end

  pipeline :person do
    plug EnsurePrivilege, [:admin, :person]
  end

  pipeline :require_portal_pass do
    plug VerifyPortalPass
  end

  # Other scopes may use custom stacks.
  # scope "/api", MetamorphicWeb do
  #   pipe_through :api
  # end

  # Enables LiveDashboard only for development
  #
  # If you want to use the LiveDashboard in production, you should put
  # it behind authentication and allow only admins to access it.
  # If your application does not have an admins-only section yet,
  # you can use Plug.BasicAuth to set up some basic authentication
  # as long as you are also using SSL (which you should anyway).
  #
  # We are using live dash in production. It is behind the admin
  # authentication and authorization.

  # Enables displaying emails via Bamboo LocalAdapter for
  # development.
  if Mix.env() == :dev do
    # If using Phoenix
    forward "/sent_emails", Bamboo.SentEmailViewerPlug
  end

  # Health check
  scope "/", MetamorphicWeb do
    pipe_through [:browser, :marketing_layout]

    get "/healthz", HealthCheckController, :index, as: :health_check
  end

  # Global Privacy Control route.
  scope "/", MetamorphicWeb do
    pipe_through [:gpc]

    get "/.well-known/gpc.json", GlobalPrivacyController, :global_privacy_response
  end

  ##
  ## Requires AUTHENTICATED person routes
  ##

  # 2FA (two-factor authentication).
  scope "/", MetamorphicWeb do
    pipe_through [:browser, :register_layout, :require_authenticated_person, :person]

    get "/people/totp", PersonTOTPController, :new
    post "/people/totp", PersonTOTPController, :create
  end

  # Subscriptions
  scope "/", MetamorphicWeb do
    pipe_through [
      :browser,
      :register_layout,
      :require_authenticated_person,
      :redirect_if_person_has_subscription
    ]

    live "/subscriptions/new", SubscriptionLive.New, :new
  end

  # Dashboard, settings, and support.
  scope "/", MetamorphicWeb do
    pipe_through [
      :browser,
      :app_layout,
      :require_authenticated_person,
      :redirect_if_admin,
      :person
    ]

    live_session :authenticated_redirect_if_admin,
      root_layout: {MetamorphicWeb.LayoutView, :root},
      on_mount: [{MetamorphicWeb.PersonLiveAuth, :redirect_if_admin}, MetamorphicWeb.Nav] do
      live "/dashboard", DashboardLive.Index, :index, as: :dashboard
      live "/people/settings", PersonSettingsLive.Index, :index, as: :person_settings
      live "/people/settings/billing", PersonSettingsLive.Billing, :billing, as: :person_settings
      live "/people/settings/data", PersonSettingsLive.Data, :data, as: :person_settings
      live "/people/settings/security", PersonSettingsLive.Security, :security, as: :person_settings
      live "/support", SupportLive.Index, :index, as: :support
    end

    get "/people/settings/data/download-stripe-data",
        PersonSettingsController,
        :download_stripe_data

    get "/people/settings/data/download-encrypted-person",
        PersonSettingsController,
        :download_encrypted_person_data

    get "/people/settings/data/download-encrypted-memories",
        PersonSettingsController,
        :download_encrypted_memory_data

    get "/people/settings/data/download-encrypted-letters",
        PersonSettingsController,
        :download_encrypted_letter_data

    get "/people/settings/data/download-encrypted-portals",
        PersonSettingsController,
        :download_encrypted_portal_data

    get "/people/settings/data/download-encrypted-person-relationships",
        PersonSettingsController,
        :download_encrypted_relationship_person_data

    get "/people/settings/data/download-encrypted-relation-relationships",
        PersonSettingsController,
        :download_encrypted_relationship_relation_data

    get "/people/settings/confirm_email/:token", PersonSettingsController, :confirm_email
    put "/people/settings/update_password", PersonSettingsController, :update_password
  end

  # Log out.
  scope "/", MetamorphicWeb do
    pipe_through [:browser, :app_layout, :require_authenticated_person, :person]

    delete "/people/log_out", PersonSessionController, :delete
  end

  # Letters, memories, relationships, portals (:index, :new, and :edit).
  scope "/", MetamorphicWeb do
    pipe_through [
      :browser,
      :app_layout,
      :require_authenticated_person,
      :require_active_subscription,
      :person
    ]

    live_session :authenticated,
      root_layout: {MetamorphicWeb.LayoutView, :root},
      on_mount: [{MetamorphicWeb.PersonLiveAuth, :ensure_authenticated}, MetamorphicWeb.Nav] do
      live "/letters", LetterLive.Index, :index, as: :letters
      live "/letters/new", LetterLive.Index, :new, as: :letters
      live "/letters/mailbox", LetterLive.Mailbox, :mailbox, as: :letters
      live "/letters/mailbox/new", LetterLive.Mailbox, :compose, as: :letters
      live "/letters/:id/read", LetterLive.Mailbox, :read, as: :letters
      # live "/letters/:id/edit", LetterLive.Index, :edit, as: :letters

      live "/memories", MemoryLive.Index, :index, as: :memories
      # live "/memories/:id/edit", MemoryLive.Index, :edit, as: :memories
      live "/memories/new", MemoryLive.Index, :new, as: :memories

      get "/memories/shared/download", MemoryDownloadsController, :download_shared_memory,
        as: :memories

      get "/memories/download", MemoryDownloadsController, :download_memory, as: :memories
      # live "/memories/:id", MemoryLive.Show, :show, as: :memories
      # live "/memories/:id/show/edit", MemoryLive.Show, :edit

      live "/portals", PortalLive.Index, :index, as: :portal
      # live "/portals/:id/edit", PortalLive.Index, :edit, as: :portal
      live "/portals/new", PortalLive.New, :new, as: :portal

      live "/relationships", RelationshipLive.Index, :index, as: :relationships
      live "/relationships/new", RelationshipLive.Index, :new, as: :relationships
      live "/relationships/:id/edit", RelationshipLive.Index, :edit, as: :relationships
      # live "/relationships/:id", RelationshipLive.Show, :show
      # live "/relationships/:id/show/edit", RelationshipLive.Show, :edit

      live "/roadmap", RoadmapLive.Index, :index, as: :roadmap
    end
  end

  # Portals (:show) with :require_portal_pass plug.
  scope "/", MetamorphicWeb do
    pipe_through [
      :browser,
      :app_layout,
      :require_authenticated_person,
      :require_active_subscription,
      :require_portal_pass,
      :person
    ]

    live "/portals/open/:slug", PortalLive.Show, :show, as: :portal
  end

  ##
  ## Does NOT require authenticated person routes
  ##

  # Registration and log in pages for people's accounts.

  scope "/", MetamorphicWeb do
    pipe_through [:browser, :register_layout, :redirect_if_person_is_authenticated]

    get "/people/log_in", PersonSessionController, :new
    post "/people/log_in", PersonSessionController, :create

    live_session :register,
      root_layout: {MetamorphicWeb.LayoutView, :register},
      on_mount: {MetamorphicWeb.PersonLiveAuth, :register} do
      live "/people/register", PersonRegistrationLive.New, :new, as: :person_registration
    end

    # get "/people/reset_password", PersonResetPasswordController, :new
    # post "/people/reset_password", PersonResetPasswordController, :create
    # live "/people/reset_password/:token", PersonResetPasswordLive.Edit, :edit, as: :person_reset_password
    # put "/people/reset_password/:token", PersonResetPasswordController, :update
  end

  # Resend confirmation instructions for a person's account.

  scope "/", MetamorphicWeb do
    pipe_through [:browser, :register_layout]

    get "/people/confirm", PersonConfirmationController, :new
    post "/people/confirm", PersonConfirmationController, :create
    get "/people/confirm/:token", PersonConfirmationController, :confirm
    get "/invitations/confirm", InviteConfirmationController, :new
    post "/invitations/confirm", InviteConfirmationController, :create
    get "/invitations/confirm/:token", InviteConfirmationController, :confirm
  end

  # Marketing pages.
  scope "/", MetamorphicWeb do
    pipe_through [:browser, :marketing_layout]

    # live_session :pre_release, root_layout: {MetamorphicWeb.LayoutView, :pre_release}, on_mount: {MetamorphicWeb.PersonLiveAuth, :pre_release} do
    # live "/", PageLive.Index, :index, as: :page
    # end

    live_session :marketing,
      root_layout: {MetamorphicWeb.LayoutView, :marketing},
      on_mount: {MetamorphicWeb.PersonLiveAuth, :marketing} do
      live "/", PageLive.Index, :index, as: :page
      live "/about", AboutLive.Index, :index, as: :about
      live "/built-to-code", BuiltToCodeLive.Index, :index, as: :built_to_code
      live "/customer-rights", CustomerRightsLive.Index, :index, as: :customer_rights
      live "/faq", FaqLive.Index, :index, as: :faq
      live "/features", FeaturesLive.Index, :index, as: :features
      live "/find-your-peace", FindYourPeaceLive.Index, :index, as: :find_your_peace
      live "/hassle-free", HassleFreeLive.Index, :index, as: :hassle_free
      live "/pricing", PricingLive.Index, :index, as: :pricing
      live "/privacy", PrivacyLive.Index, :index, as: :privacy
      live "/security", SecurityLive.Index, :index, as: :security
      live "/rocks", SpecialThanksLive.Index, :index, as: :thanks
      live "/terms", TermsLive.Index, :index, as: :terms
      live "/why-metamorphic", WhyMetamorphicLive.Index, :index, as: :why_metamorphic

      live "/blog", BlogLive.Index, :index, as: :blog

      live "/blog/clean-cookies", BlogLive.Posts.CleanCookies, :index,
        as: :blog_posts_clean_cookies

      live "/blog/early-access-sign-ups", BlogLive.Posts.EarlyAccessSignUps, :index,
        as: :blog_posts_early_access_sign_ups

      live "/blog/markdown-guide", BlogLive.Posts.MarkdownGuide, :index,
        as: :blog_posts_markdown_guide
    end
  end

  # Pre-release invitations page.
  # scope "/", MetamorphicWeb do
  #  pipe_through [:browser, :pre_release_layout]

  # live "/", PageLive.Index, :index, as: :page
  # end

  ##
  ## Requires ADMIN AUTHENTICATED routes.
  ##

  scope "/odus", MetamorphicWeb do
    pipe_through [:browser, :app_layout, :require_authenticated_person, :admin]
    live_dashboard "/sys_dash", metrics: MetamorphicWeb.Telemetry
  end

  scope "/odus", MetamorphicWeb do
    pipe_through [:browser, :app_layout, :require_authenticated_person, :admin]

    live_session :admin,
      root_layout: {MetamorphicWeb.LayoutView, :root},
      on_mount: [{MetamorphicWeb.PersonLiveAuth, :admin}, MetamorphicWeb.Nav] do
      live "/dashboard", DashboardLive.Admin, :admin, as: :admin_dashboard
      live "/invitations", InviteLive.Admin, :admin, as: :admin_invite
      live "/invitations/new", InviteLive.Admin, :admin_new, as: :admin_invite_new
      live "/invitations/:id/edit", InviteLive.Admin, :admin_edit, as: :admin_invite_edit
      live "/invitations/:id", InviteLive.Show, :show
      live "/invitations/:id/show/edit", InviteLive.Show, :edit
      live "/people/settings", PersonSettingsLive.Admin, :admin, as: :admin_settings
      live "/roadmap", RoadmapLive.Admin, :admin, as: :admin_roadmap
      live "/roadmap/new", RoadmapLive.Admin, :admin_new, as: :admin_roadmap_new
      live "/roadmap/:id/edit", RoadmapLive.Admin, :admin_edit, as: :admin_roadmap_edit
      live "/roadmap/:id/update", RoadmapLive.Admin, :admin_update, as: :admin_roadmap_update
      live "/roadmap/:id/delete", RoadmapLive.Admin, :admin_delete, as: :admin_roadmap_delete
      live "/support", SupportLive.Admin, :admin, as: :admin_support
    end

    get "/people/settings/data/download-encrypted-admin",
        PersonSettingsController,
        :download_encrypted_admin_data

    get "/people/settings/confirm_email/:token", PersonSettingsController, :confirm_email
    put "/people/settings/update_password", PersonSettingsController, :update_password
    delete "/people/log_out", PersonSessionController, :delete
  end
end

To update:

If I explicitly add <%= csrf_input_tag("/login") %> to my login form, then the csrf_token is added and everything works on latest Phoenix version.

I’m not sure why the _csrf_token was getting added in earlier versions of Phoenix (without explicitly setting) but not the most recent, but I imagine I was doing something wrong.

I was routing from a static login to a live view and my form on the login page was written like so…

<.form let={f} for={@conn} url={Routes.person_session_path(@conn, :create)} as={:person}>
  ...
</.form>

Doing this has made it work again:

<.form let={f} for={@conn} url={Routes.person_session_path(@conn, :create)} as={:person}>
  <%= csrf_input_tag("/login") %>
  ...
</.form>

We had the same issue with our activation page and had to add the csrf_input_tag in manually. I am assuming this is a bug.

2 Likes

Another solution will be to change the url option to action. It seems like the csrf token only gets added if the action gets set.

<.form let={f} for={@conn} action={Routes.person_session_path(@conn, :create)} as={:person}>
  ...
</.form>
1 Like

This is the breaking change: Do not generate csrf tokens for forms with no action · phoenixframework/phoenix_live_view@e9fd7fa · GitHub

1 Like

Thank you!

Switching from prod to dev environment on same machine, does not reset/add csrf token. And login goes to error.