Closed treeman closed 3 years ago
:wave: hello!
I believe you should be able to pass params to c:Phoenix.Socket.connect/3
by adding URL query-params to the URL you're using to connect.
For example we use this trick in the test cases to simulate c:connect/3
returning :error
:
# test/support/lib/slipstream_web/channels/user_socket.ex
..
@impl Phoenix.Socket
def connect(%{"reject" => "yes"}, _socket, _connect_info) do
:error
end
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
..
And the %{"reject" => "yes"}
params are passed as URL query-params in the :uri
in configuration
# test/slipstream/integration_test.exs
..
describe "given c:Phoenix.Socket.connect/3 returns :error" do
setup do
[config: [uri: "ws://localhost:4001/socket/websocket?reject=yes"]]
end
test "the socket is disconnected with :upgrade_failure reason", c do
import Slipstream
assert {:error, {:upgrade_failure, %{status_code: 403}}} =
c.config
|> connect!()
|> await_connect(15_000)
end
end
..
So you should be able to pass the token by URL-encoding it in the :uri
configuration option
Slipstream.connect(uri: "https://example.org/socket/websocket?token=asdfasdfasdf")
Wonderful, thank you!
I wonder, if params are sent over the uri isn't there a risk of a man-in-the-middle that can see and copy the token?
I think you're right, URL params are generally discouraged for any sort of security mechanisms these days. If I remember correctly, one vector had something to do with sending traffic through a proxy and the proxy having knowledge of tokens. Not to mention that if you have any observability tooling or logging keeping track of requests, the tokens will probably make their way into the observability data or log stash which is not great. I known GitHub will complain if you try to send an API token through URL params. I'm actually kinda surprised the Phoenix.Socket
docs still recommend doing token verification by passing tokens as URL params :thinking:
One way around using URL params would be to do token checking in the [c:Phoenix.Channel.join/3
](
) callback. In fact those docs have an example of an implementation that checks authorization. You would pass the token with Slipstream.join(socket, topic, %{"token" => my_token})
, which would send the token in a WebSocket frame instead of a URL param. In order to use this for authorization, you need to allow all WebSocket connections in the c:Phoenix.Socket.connect/3
callback, which would allow a hypothetical attacker to connect many WebSocket clients to the server without authorization. I imagine WebSocket clients just sitting on a connection are not very expensive, but that could potentially add up to a DDoS.
Another approach more in-line with what GitHub recommends in that blog post is to pass authorization as a header, such as with Bearer token authorization. It looks like this is not a super small change because headers are not exposed by default in c:Phoenix.Socket.connect/3
.
First you need to configure the Phoenix.Endpoint.socket/3
to pass :x_headers
in the connect_info
argument:
defmodule MyAppWeb.Endpoint do
..
socket("/socket", MyAppWeb.UserSocket,
websocket: [connect_info: [:x_headers]],
longpoll: false
)
..
end
And then you can pass the token in an x-header (any header beginning with "x-", case insensitive)
Slipstream.connect(uri: my_uri, headers: [{"x-my-token", my_token}])
Then the token will be available in connect_info
:
defmodule MyAppWeb.UserSocket do
..
def connect(_params, socket, %{x_headers: x_headers}) do
with {:ok, proposed_token} <-
Enum.find_value(x_headers, :error, fn {k, v} -> k == "x-my-token" && {:ok, v} end),
:ok <- Tokens.authenticate_token(proposed_token) do
{:ok, socket}
else
_ ->
:error
end
end
..
end
Thanks again for a great reply, your code works nicely for me.
I did indeed follow the Socket/Token docs. I opened an issue in the Phoenix repo for this.
I'm trying out this library to create a client that connects to a pretty standard Phoenix channel on the server. I'd like to use token based authentication, as shown in the Phoenix docs. This is how it looks on the server:
And the question is: how do I pass the token from the client? There's no
params
option in the configuration and I don't know howheaders
/mint_opts
can be used here.