Repository: phoenixframework/phoenix (elixir)

Issue:
Misalignment in socket authentication documentation
Hi,

There is some misalignment in the documentation for socket authentication in the channels guide and in the generated `socket.js` file from `phx.gen.socket`.

The guide suggests using the transport agnostic method of sending the user token through a header, while the file suggests using the older approach of sending it as a query parameter.

https://github.com/phoenixframework/phoenix/blob/69e2cfa13b94e75bb749bd912f3727e9e4fd410c/guides/real_time/channels.md?plain=1#L453-L457

https://github.com/phoenixframework/phoenix/blob/2875c54cddec2fc45258baee9a84cc8193cad541/priv/templates/phx.gen.socket/socket.js#L7-L9

Pull request description:
use single source of truth for socket token authn docs
Removes outdated token authentication documentation from the generated `socket.js` file from `phx.gen.socket`. Instead, it reduces the maintenance of the generator by providing a link to the token authentication section in the guides.

Also, redacts the rest of the docs to reference the new method for authenticating.

Closes #6241.

Patch:
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index 7436cda32..eaca2fbd1 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -107,7 +107,8 @@ In your Phoenix app's `Endpoint` module, a `socket` declaration specifies which
 ```elixir
 socket "/socket", HelloWeb.UserSocket,
   websocket: true,
-  longpoll: false
+  longpoll: false,
+  auth_token: true
 ```
 
 Phoenix comes with two default transports: websocket and longpoll. You can configure them directly via the `socket` declaration.
@@ -117,7 +118,7 @@ Phoenix comes with two default transports: websocket and longpoll. You can confi
 On the client side, you will establish a socket connection to the route above:
 
 ```javascript
-let socket = new Socket("/socket", {params: {token: window.userToken}})
+let socket = new Socket("/socket", {authToken: window.userToken})
 ```
 
 On the server, Phoenix will invoke `HelloWeb.UserSocket.connect/2`, passing your parameters and the initial socket state. Within the socket, you can authenticate and identify a socket connection and set default socket assigns. The socket is also where you define your channel routes.
diff --git a/guides/real_time/presence.md b/guides/real_time/presence.md
index 32271b855..df3cec431 100644
--- a/guides/real_time/presence.md
+++ b/guides/real_time/presence.md
@@ -93,7 +93,7 @@ We can see presence working by adding the following to `assets/js/app.js`:
 ```javascript
 import {Socket, Presence} from "phoenix"
 
-let socket = new Socket("/socket", {params: {token: window.userToken}})
+let socket = new Socket("/socket", {authToken: window.userToken})
 let channel = socket.channel("room:lobby", {name: window.location.search.split("=")[1]})
 let presence = new Presence(channel)
 
diff --git a/lib/phoenix/channel/server.ex b/lib/phoenix/channel/server.ex
index b5b4906d6..9e159e55e 100644
--- a/lib/phoenix/channel/server.ex
+++ b/lib/phoenix/channel/server.ex
@@ -295,6 +295,9 @@ defmodule Phoenix.Channel.Server do
     Process.put(:"$initial_call", {channel, :join, 3})
     Process.put(:"$callers", [pid])
 
+    # TODO: replace with Process.put_label/2 when we require Elixir 1.17
+    Process.put(:"$process_label", {Phoenix.Channel, channel, topic})
+
     socket = %{
       socket
       | channel_pid: self(),
diff --git a/lib/phoenix/debug.ex b/lib/phoenix/debug.ex
new file mode 100644
index 000000000..4f86ae697
--- /dev/null
+++ b/lib/phoenix/debug.ex
@@ -0,0 +1,168 @@
+defmodule Phoenix.Debug do
+  @moduledoc """
+  Functions for runtime introspection and debugging of Phoenix applications.
+
+  This module provides utilities for inspecting and debugging Phoenix applications.
+  At the moment, it only includes functions related to `Phoenix.Socket` and `Phoenix.Channel`
+  processes.
+
+  It allows you to:
+
+    * List all currently connected `Phoenix.Socket` transport processes.
+    * List all channels for a given `Phoenix.Socket` process.
+    * Get the socket of a channel process.
+    * Check if a process is a `Phoenix.Socket` or `Phoenix.Channel`.
+
+  """
+
+  @doc """
+  Returns a list of all currently connected `Phoenix.Socket` transport processes.
+
+  Note that custom sockets implementing the `Phoenix.Socket.Transport` behaviour
+  are not listed.
+
+  Each process corresponds to one connection that can have multiple channels.
+
+  For example, when using Phoenix LiveView, the browser establishes a socket
+  connection when initially navigating to the page, and each live navigation
+  retains the same socket connection. Nested LiveViews also share the same
+  connection, each being a different channel. See `Phoenix.Debug.channels/1`.
+
+  ## Examples
+
+      iex> Phoenix.Debug.list_sockets()
+      [%{pid: #PID<0.123.0>, module: Phoenix.LiveView.Socket, id: nil}]
+
+  """
+  def list_sockets do
+    for pid <- Process.list(), dict = socket_process_dict(pid), not is_nil(dict) do
+      {Phoenix.Socket, mod, id} = keyfind(dict, :"$process_label")
+      %{pid: pid, module: mod, id: id}
+    end
+  end
+
+  defp keyfind(list, key) do
+    case List.keyfind(list, key, 0) do
+      {^key, value} -> value
+      _ -> nil
+    end
+  end
+
+  defp socket_process_dict(pid) do
+    # Phoenix.Socket sets the "$process_label" to {Phoenix.Socket, handler_module, id}
+    with info when is_list(info) <- Process.info(pid, [:dictionary]),
+         dictionary when not is_nil(dictionary) <- keyfind(info, :dictionary),
+         label when not is_nil(label) <- keyfind(dictionary, :"$process_label"),
+         {Phoenix.Socket, mod, id} when is_atom(mod) and (is_binary(id) or is_nil(id)) <- label do
+      dictionary
+    else
+      _ -> nil
+    end
+  end
+
+  @doc """
+  Returns true if the given pid is a `Phoenix.Socket` transport process.
+
+  It returns `false` for custom sockets implementing the `Phoenix.Socket.Transport` behaviour.
+
+  ## Examples
+
+      iex> Phoenix.Debug.list_sockets() |> Enum.at(0) |> Map.fetch!(:pid) |> socket_process?()
+      true
+
+      iex> socket_process?(pid(0,456,0))
+      false
+
+  """
+  def socket_process?(pid) do
+    not is_nil(socket_process_dict(pid))
+  end
+
+  @doc """
+  Checks if the given pid is a `Phoenix.Channel` process.
+
+  Note: this function returns false for [custom channels](https://hexdocs.pm/phoenix/Phoenix.Socket.html#module-custom-channels).
+  """
+  def channel_process?(pid) do
+    # Phoenix.Channel sets the "$process_label" to {Phoenix.Socket, handler_module, id}
+    with info when is_list(info) <- Process.info(pid, [:dictionary]),
+         dictionary when not is_nil(dictionary) <- keyfind(info, :dictionary),
+         label when not is_nil(label) <- keyfind(dictionary, :"$process_label"),
+         {Phoenix.Channel, mod, topic} when is_atom(mod) and is_binary(topic) <- label do
+      true
+    else
+      _ -> false
+    end
+  end
+
+  @doc """
+  Returns a list of all currently connected channels for the given `Phoenix.Socket` pid.
+
+  Each channel is represented as a map with the following keys:
+
+    - `:pid` - the pid of the channel process
+    - `:status` - the status of the channel
+    - `:topic` - the topic of the channel
+
+  Note that this list also contains [custom channels](https://hexdocs.pm/phoenix/Phoenix.Socket.html#module-custom-channels)
+  like LiveViews. You can check if a channel is a custom channel by using the `channel?/1`
+  function, which returns `false` for custom channels.
+
+  ## Examples
+
+      iex> pid = Phoenix.Debug.list_sockets() |> Enum.at(0) |> Map.fetch!(:pid)
+      iex> Phoenix.Debug.list_channels(pid)
+      {:ok,
+       [
+         %{pid: #PID<0.1702.0>, status: :joined, topic: "lv:phx-GDp9a9UZPiTxcgnE"},
+         %{pid: #PID<0.1727.0>, status: :joined, topic: "lv:sidebar"}
+       ]}
+
+      iex> Phoenix.Debug.list_channels(pid(0,456,0))
+      {:error, :not_alive}
+
+  """
+  def list_channels(socket_pid) do
+    ref = make_ref()
+
+    if Process.alive?(socket_pid) and socket_process?(socket_pid) do
+      send(socket_pid, {:debug_channels, ref, self()})
+
+      receive do
+        {:debug_channels, ^ref, channels} ->
+          {:ok, channels}
+      after
+        5_000 -> {:error, :timeout}
+      end
+    else
+      {:error, :not_alive}
+    end
+  end
+
+  @doc """
+  Returns the socket of the channel process.
+
+  Note: this only works for channels defined with `use Phoenix.Channel`.
+  For LiveViews, use the functions defined in `Phoenix.LiveView.Debug` instead.
+
+  ## Examples
+
+      iex> pid = Phoenix.Debug.list_sockets() |> Enum.at(0) |> Map.fetch!(:pid)
+      iex> {:ok, channels} = Phoenix.Debug.list_channels(pid)
+      iex> channels |> Enum.at(0) |> Map.fetch!(:pid) |> socket()
+      {:ok, %Phoenix.Socket{...}}
+
+      iex> socket(pid(0,456,0))
+      {:error, :not_alive_or_not_a_channel}
+
+  """
+  def socket(channel_pid) do
+    if channel_process?(channel_pid) do
+      {:ok, Phoenix.Channel.Server.socket(channel_pid)}
+    else
+      {:error, :not_alive_or_not_a_channel}
+    end
+  catch
+    :exit, _ -> {:error, :not_alive_or_not_a_channel}
+  end
+end
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index b684d0f9a..e37048010 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -709,7 +709,8 @@ defmodule Phoenix.Endpoint do
       :check_csrf,
       :code_reloader,
       :connect_info,
-      :auth_token
+      :auth_token,
+      :log
     ]
 
     websocket =
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 00c4885c6..25ada6161 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -520,7 +520,13 @@ defmodule Phoenix.Socket do
   defp result(:error), do: :error
   defp result({:error, _}), do: :error
 
+  defp set_label(socket) do
+    # TODO: replace with Process.put_label/2 when we require Elixir 1.17
+    Process.put(:"$process_label", {Phoenix.Socket, socket.handler, socket.id})
+  end
+
   def __init__({state, %{id: id, endpoint: endpoint} = socket}) do
+    set_label(socket)
     _ = id && endpoint.subscribe(id)
     {:ok, {state, %{socket | transport_pid: self()}}}
   end
@@ -563,6 +569,16 @@ defmodule Phoenix.Socket do
     {:ok, state}
   end
 
+  def __info__({:debug_channels, ref, reply_to}, {state, socket}) do
+    channels =
+      Enum.map(state.channels, fn {topic, {pid, _monitor_ref, status}} ->
+        %{topic: topic, pid: pid, status: status}
+      end)
+
+    send(reply_to, {:debug_channels, ref, channels})
+    {:ok, {state, socket}}
+  end
+
   def __info__(_, state) do
     {:ok, state}
   end
@@ -625,6 +641,8 @@ defmodule Phoenix.Socket do
             {:ok, {state, socket}}
 
           id when is_binary(id) ->
+            # update the process label
+            set_label(socket)
             {:ok, {state, %{socket | id: id}}}
 
           invalid ->
diff --git a/priv/templates/phx.gen.socket/socket.js b/priv/templates/phx.gen.socket/socket.js
index a308171db..236d998b0 100644
--- a/priv/templates/phx.gen.socket/socket.js
+++ b/priv/templates/phx.gen.socket/socket.js
@@ -5,52 +5,11 @@
 import {Socket} from "phoenix"
 
 // And connect to the path in "<%= web_prefix %>/endpoint.ex". We pass the
-// token for authentication. Read below how it should be used.
-let socket = new Socket("/socket", {params: {token: window.userToken}})
-
-// When you connect, you'll often need to authenticate the client.
-// For example, imagine you have an authentication plug, `MyAuth`,
-// which authenticates the session and assigns a `:current_user`.
-// If the current user exists you can assign the user's token in
-// the connection for use in the layout.
-//
-// In your "<%= web_prefix %>/router.ex":
-//
-//     pipeline :browser do
-//       ...
-//       plug MyAuth
-//       plug :put_user_token
-//     end
-//
-//     defp put_user_token(conn, _) do
-//       if current_user = conn.assigns[:current_user] do
-//         token = Phoenix.Token.sign(conn, "user socket", current_user.id)
-//         assign(conn, :user_token, token)
-//       else
-//         conn
-//       end
-//     end
-//
-// Now you need to pass this token to JavaScript. You can do so
-// inside a script tag in "<%= web_prefix %>/components/layouts/root.html.heex":
-//
-//     <script>window.userToken = "<%%= assigns[:user_token] %>";</script>
-//
-// You will need to verify the user token in the "connect/3" function
-// in "<%= web_prefix %>/channels/user_socket.ex":
-//
-//     def connect(%{"token" => token}, socket, _connect_info) do
-//       # max_age: 1209600 is equivalent to two weeks in seconds
-//       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do
-//         {:ok, user_id} ->
-//           {:ok, assign(socket, :user, user_id)}
-//
-//         {:error, reason} ->
-//           :error
-//       end
-//     end
+// token for authentication.
 //
-// Finally, connect to the socket:
+// Read the [`Using Token Authentication`](https://hexdocs.pm/phoenix/channels.html#using-token-authentication)
+// section to see how the token should be used.
+let socket = new Socket("/socket", {authToken: window.userToken})
 socket.connect()
 
 // Now that you are connected, you can join channels with a topic.


Test patch:
diff --git a/test/mix/tasks/phx.gen.socket_test.exs b/test/mix/tasks/phx.gen.socket_test.exs
index 7fa277374..db83baa14 100644
--- a/test/mix/tasks/phx.gen.socket_test.exs
+++ b/test/mix/tasks/phx.gen.socket_test.exs
@@ -33,7 +33,7 @@ defmodule Mix.Tasks.Phx.Gen.SocketTest do
         assert file =~ ~S|// you uncomment its entry in "assets/js/app.js".|
 
         assert file =~ ~S|// And connect to the path in "lib/phoenix_web/endpoint.ex".|
-        assert file =~ ~S|let socket = new Socket("/socket", {params: {token: window.userToken}})|
+        assert file =~ ~S|let socket = new Socket("/socket", {authToken: window.userToken})|
 
         assert file =~ ~S|let channel = socket.channel("room:42", {})|
         assert file =~ ~S|channel.join()|
@@ -74,7 +74,9 @@ defmodule Mix.Tasks.Phx.Gen.SocketTest do
         assert file =~ ~S|import {Socket} from "phoenix"|
 
         assert file =~ ~S|// And connect to the path in "lib/phoenix/endpoint.ex".|
-        assert file =~ ~S|In your "lib/phoenix/router.ex":|
+
+        assert file =~
+                 ~S|Read the [`Using Token Authentication`](https://hexdocs.pm/phoenix/channels.html#using-token-authentication)|
 
         assert file =~ ~S|let channel = socket.channel("room:42", {})|
         assert file =~ ~S|channel.join()|
@@ -101,9 +103,10 @@ defmodule Mix.Tasks.Phx.Gen.SocketTest do
         assert file =~ ~S|import {Socket} from "phoenix"|
 
         assert file =~ ~S|// And connect to the path in "lib/phoenix_web/endpoint.ex".|
-        assert file =~ ~S|let socket = new Socket("/socket", {params: {token: window.userToken}})|
+        assert file =~ ~S|let socket = new Socket("/socket", {authToken: window.userToken})|
 
-        assert file =~ ~S|In your "lib/phoenix_web/router.ex":|
+        assert file =~
+                 ~S|Read the [`Using Token Authentication`](https://hexdocs.pm/phoenix/channels.html#using-token-authentication)|
 
         assert file =~ ~S|let channel = socket.channel("room:42", {})|
         assert file =~ ~S|channel.join()|
diff --git a/test/phoenix/debug_test.exs b/test/phoenix/debug_test.exs
new file mode 100644
index 000000000..b8bc76951
--- /dev/null
+++ b/test/phoenix/debug_test.exs
@@ -0,0 +1,102 @@
+defmodule Phoenix.DebugTest do
+  use ExUnit.Case, async: true
+
+  alias Phoenix.Debug
+
+  # we cannot easily test the Phoenix.Debug functions with the regular
+  # Phoenix.ChannelTest functions, because they use the test process
+  # itself as the transport process
+  defmodule FakeSocket do
+    use GenServer
+
+    def init(channel_pid) do
+      Process.put(:"$process_label", {Phoenix.Socket, __MODULE__, nil})
+      {:ok, channel_pid}
+    end
+
+    def handle_info({:debug_channels, ref, reply_to}, state) do
+      send(
+        reply_to,
+        {:debug_channels, ref, [%{pid: state, status: :joined, topic: "room:lobby"}]}
+      )
+
+      {:noreply, state}
+    end
+  end
+
+  defmodule FakeChannel do
+    use GenServer
+
+    def init(state) do
+      Process.put(:"$process_label", {Phoenix.Channel, __MODULE__.Channel, "room:lobby"})
+      {:ok, state}
+    end
+
+    def handle_call(:socket, _from, state) do
+      {:reply, %Phoenix.Socket{}, state}
+    end
+  end
+
+  setup do
+    {:ok, channel_pid} = GenServer.start_link(FakeChannel, nil)
+    {:ok, socket_pid} = GenServer.start_link(FakeSocket, channel_pid)
+
+    %{socket_pid: socket_pid, channel_pid: channel_pid}
+  end
+
+  describe "list_sockets/0" do
+    test "returns a list of all currently connected channel socket processes", %{
+      socket_pid: socket_pid
+    } do
+      sockets = Debug.list_sockets()
+      assert is_list(sockets)
+      assert Enum.any?(sockets, fn s -> s.pid == socket_pid end)
+      assert Enum.find(sockets, fn s -> s.module == __MODULE__.FakeSocket end)
+    end
+  end
+
+  describe "socket_process?/1" do
+    test "returns true if the given pid is a channel socket process", %{socket_pid: socket_pid} do
+      assert Debug.socket_process?(socket_pid)
+    end
+
+    test "returns false for a non-socket process" do
+      refute Debug.socket_process?(self())
+    end
+  end
+
+  describe "channel_process?/1" do
+    test "returns true if the given pid is a channel process", %{channel_pid: channel_pid} do
+      assert Debug.channel_process?(channel_pid)
+    end
+
+    test "returns false for a non-channel process" do
+      refute Debug.channel_process?(self())
+    end
+  end
+
+  describe "list_channels/1" do
+    test "returns a list of all channels for a given socket pid", %{
+      socket_pid: socket_pid,
+      channel_pid: channel_pid
+    } do
+      {:ok, channels} = Debug.list_channels(socket_pid)
+      assert is_list(channels)
+      assert Enum.any?(channels, fn ch -> ch.pid == channel_pid and ch.topic == "room:lobby" end)
+    end
+
+    test "returns error for non-socket pid" do
+      assert {:error, :not_alive} = Debug.list_channels(self())
+    end
+  end
+
+  describe "socket/1" do
+    test "returns the socket struct for a channel process", %{channel_pid: channel_pid} do
+      assert {:ok, %Phoenix.Socket{}} = Debug.socket(channel_pid)
+    end
+
+    test "returns error for non-channel process" do
+      assert {:error, :not_alive_or_not_a_channel} = Debug.socket(self())
+    end
+  end
+end