Repository: phoenixframework/phoenix
Pull request #4676 (2022-02-01 15:01:32)

Set `live_socket_id` when restoring session from cookie
When restoring user sessions from the "remember me"-cookie, the `live_socket_id` value is not set, so the `log_out_*`-function does not disconnect sockets for users that are on a "restored" session.

This PR sets a `live_socket_id` on the session when creating a new session from the "remember me"-cookie and adds an assert in the test suite for this behaviour. 


Patch:
diff --git a/installer/templates/phx_assets/app.css b/installer/templates/phx_assets/app.css
index 19c2e51ed..bc285211c 100644
--- a/installer/templates/phx_assets/app.css
+++ b/installer/templates/phx_assets/app.css
@@ -46,7 +46,7 @@
   transition: opacity 1s ease-out;
 }
 
-.phx-loading{
+.phx-loading {
   cursor: wait;
 }
 
@@ -99,22 +99,22 @@
   animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
 }
 
-@keyframes fade-in-scale-keys{
+@keyframes fade-in-scale-keys {
   0% { scale: 0.95; opacity: 0; }
   100% { scale: 1.0; opacity: 1; }
 }
 
-@keyframes fade-out-scale-keys{
+@keyframes fade-out-scale-keys {
   0% { scale: 1.0; opacity: 1; }
   100% { scale: 0.95; opacity: 0; }
 }
 
-@keyframes fade-in-keys{
+@keyframes fade-in-keys {
   0% { opacity: 0; }
   100% { opacity: 1; }
 }
 
-@keyframes fade-out-keys{
+@keyframes fade-out-keys {
   0% { opacity: 1; }
   100% { opacity: 0; }
 }
diff --git a/installer/templates/phx_web/endpoint.ex b/installer/templates/phx_web/endpoint.ex
index 2acb2880c..1f42e1d2f 100644
--- a/installer/templates/phx_web/endpoint.ex
+++ b/installer/templates/phx_web/endpoint.ex
@@ -7,7 +7,8 @@ defmodule <%= @endpoint_module %> do
   @session_options [
     store: :cookie,
     key: "_<%= @web_app_name %>_key",
-    signing_salt: "<%= @signing_salt %>"
+    signing_salt: "<%= @signing_salt %>",
+    same_site: "Lax"
   ]
 
   <%= if !(@dashboard || @live) do %><%= "# " %><% end %>socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index e8155d84b..067fa5048 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -1081,20 +1081,17 @@ defmodule Phoenix.Controller do
 
   It sets the following headers:
 
+    * `referrer-policy` - only send origin on cross origin requests
     * `x-frame-options` - set to SAMEORIGIN to avoid clickjacking
       through iframes unless in the same origin
     * `x-content-type-options` - set to nosniff. This requires
       script and style tags to be sent with proper content type
-    * `x-xss-protection` - set to "1; mode=block" to improve XSS
-      protection on both Chrome and IE
     * `x-download-options` - set to noopen to instruct the browser
       not to open a download directly in the browser, to avoid
       HTML files rendering inline and accessing the security
       context of the application (like critical domain cookies)
     * `x-permitted-cross-domain-policies` - set to none to restrict
       Adobe Flash Player’s access to data
-    * `cross-origin-window-policy` - set to deny to avoid window
-      control attacks
 
   A custom headers map may also be given to be merged with defaults.
   It is recommended for custom header keys to be in lowercase, to avoid sending
@@ -1113,12 +1110,14 @@ defmodule Phoenix.Controller do
   end
   defp put_secure_defaults(conn) do
     merge_resp_headers(conn, [
-      {"x-frame-options", "SAMEORIGIN"},
-      {"x-xss-protection", "1; mode=block"},
+      # Below is the default from November 2020 but not yet in Safari as in Jan/2022.
+      # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+      {"referrer-policy", "strict-origin-when-cross-origin"},
       {"x-content-type-options", "nosniff"},
+      # Applies only to Internet Explorer, can safely be removed in the future.
       {"x-download-options", "noopen"},
-      {"x-permitted-cross-domain-policies", "none"},
-      {"cross-origin-window-policy", "deny"}
+      {"x-frame-options", "SAMEORIGIN"},
+      {"x-permitted-cross-domain-policies", "none"}
     ])
   end
 
diff --git a/lib/phoenix/socket/pool_supervisor.ex b/lib/phoenix/socket/pool_supervisor.ex
index bc6e25755..bf9c1c27d 100644
--- a/lib/phoenix/socket/pool_supervisor.ex
+++ b/lib/phoenix/socket/pool_supervisor.ex
@@ -2,8 +2,13 @@ defmodule Phoenix.Socket.PoolSupervisor do
   @moduledoc false
   use Supervisor
 
-  def start_link(args) do
-    Supervisor.start_link(__MODULE__, args)
+  # TODO: Use PartitionSupervisor once we require Elixir v1.14
+  def start_link({endpoint, name, partitions}) do
+    Supervisor.start_link(
+      __MODULE__,
+      {endpoint, name, partitions},
+      name: Module.concat(endpoint, name)
+    )
   end
 
   def start_child(endpoint, name, key, spec) do
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index 1e0ba9764..bfca97ecc 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -30,8 +30,7 @@ defmodule <%= inspect auth_module %> do
 
     conn
     |> renew_session()
-    |> put_session(:<%= schema.singular %>_token, token)
-    |> put_session(:live_socket_id, "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}")
+    |> put_token_in_session(token)
     |> maybe_write_remember_me_cookie(token, params)
     |> redirect(to: <%= schema.singular %>_return_to || signed_in_path(conn))
   end
@@ -95,13 +94,13 @@ defmodule <%= inspect auth_module %> do
   end
 
   defp ensure_<%= schema.singular %>_token(conn) do
-    if <%= schema.singular %>_token = get_session(conn, :<%= schema.singular %>_token) do
-      {<%= schema.singular %>_token, conn}
+    if token = get_session(conn, :<%= schema.singular %>_token) do
+      {token, conn}
     else
       conn = fetch_cookies(conn, signed: [@remember_me_cookie])
 
-      if <%= schema.singular %>_token = conn.cookies[@remember_me_cookie] do
-        {<%= schema.singular %>_token, put_session(conn, :<%= schema.singular %>_token, <%= schema.singular %>_token)}
+      if token = conn.cookies[@remember_me_cookie] do
+        {token, put_token_in_session(conn, token)}
       else
         {nil, conn}
       end
@@ -139,6 +138,12 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
+  defp put_token_in_session(conn, token) do
+    conn
+    |> put_session(:<%= schema.singular %>_token, token)
+    |> put_session(:live_socket_id, "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}")
+  end
+
   defp maybe_store_return_to(%{method: "GET"} = conn) do
     put_session(conn, :<%= schema.singular %>_return_to, current_path(conn))
   end


Test patch:
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index bf26929a8..5ac85fd14 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -101,8 +101,11 @@ defmodule <%= inspect auth_module %>Test do
         |> put_req_cookie(@remember_me_cookie, signed_token)
         |> <%= inspect schema.alias %>Auth.fetch_current_<%= schema.singular %>([])
 
-      assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token
       assert conn.assigns.current_<%= schema.singular %>.id == <%= schema.singular %>.id
+      assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token
+
+      assert get_session(conn, :live_socket_id) ==
+               "<%= schema.plural %>_sessions:#{Base.url_encode64(<%= schema.singular %>_token)}"
     end
 
     test "does not authenticate if data is missing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 97ecf081b..2ebe628e6 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -589,19 +589,15 @@ defmodule Phoenix.Controller.ControllerTest do
   test "put_secure_browser_headers/2" do
     conn = conn(:get, "/") |> put_secure_browser_headers()
     assert get_resp_header(conn, "x-frame-options") == ["SAMEORIGIN"]
-    assert get_resp_header(conn, "x-xss-protection") == ["1; mode=block"]
     assert get_resp_header(conn, "x-content-type-options") == ["nosniff"]
     assert get_resp_header(conn, "x-download-options") == ["noopen"]
     assert get_resp_header(conn, "x-permitted-cross-domain-policies") == ["none"]
-    assert get_resp_header(conn, "cross-origin-window-policy") == ["deny"]
 
     custom_headers = %{"x-frame-options" => "custom", "foo" => "bar"}
     conn = conn(:get, "/") |> put_secure_browser_headers(custom_headers)
     assert get_resp_header(conn, "x-frame-options") == ["custom"]
-    assert get_resp_header(conn, "x-xss-protection") == ["1; mode=block"]
     assert get_resp_header(conn, "x-download-options") == ["noopen"]
     assert get_resp_header(conn, "x-permitted-cross-domain-policies") == ["none"]
-    assert get_resp_header(conn, "cross-origin-window-policy") == ["deny"]
     assert get_resp_header(conn, "foo") == ["bar"]
   end