Phoenix Limit Sockets per user

Hello,

I have recently been working with Phoenix Channels. The whole process has been incredibly straightforward so far! The one thing I have not been able to find has been the security implications when using Channels/Sockets.

I would imagine in a production app with authentication one would want to limit both the number of sockets a single user can have open at once and the number of channels connected.

Currently we limit the number of sessions already so we could tie each user socket’s id to the access token and force disconnect when a session is closed, but that still wouldn’t stop a user from opening multiple sockets per active session.

If anyone can point me in the right direction or if my thinking is wrong about security here let me know!

3 Likes

I think there are two pretty straightforward ways for doing this:

1: Use Phoenix.Presence. This will allow you to automatically track how often a specific user is currently connected and limit those numbers accordingly.
2: Store a per-user counter in your database or create your own OTP application for holding this kind of state (e. g. using GenServer).

2 Likes

Ok #1 makes sense as an approach to limiting the channels because I can manually pass a topic and the channel pid to Presence.track/4
Can Presence be used to track at the socket level since it requires the PID of a channel and a topic?

1 Like

Great question, and I’ve recently when through the exercise myself.

First, you have to understand that blocking multiple sockets at the user_socket level is nasty, since all you can do is return :error. The client socket then gets terminated, and has no clue if it is was an authorization error or multiple sockets error, or even perhaps some other logic you have to block the connection (in my case, client update required!).

With that being said, you might not care about returning the exact error to the user, or maybe you do:

  1. If you do: You need to allow the user to connect to a socket, and then make it obligatory that all active users connect to some global channel (in my case it is global:[USER_ID]). Once you accept the socket, and the user attempts to join this global channel, it becomes trivial to detect multiple sockets using Presence, :global.where_is, or Elixir v1.4 pid Registry.
    I personally tried presence, but it ends up adding extra baggage like pushing presence state down to the client, which I don’t want. I was using :global.where_is because I am still stuck at Elixir v1.3, but it worked fine.

  2. If you don’t: Just use :global.where_is or Registry to register the user socket under their user id, then check if a process exists when another socket connection comes in.


I’ll tell you this though, at the end, I scrapped it. The reason I wanted it in the first place was because a user would end up in a game room with themselves (given they connect on two different sockets). The best solution I found and implemented was to add the checks in the room channel and game channel level not at the socket level. If the user wants to connect on multiple devices, sure why not! The real problem is elsewhere and should be solved on a different level.

3 Likes

You can intercept presence_diff in your channel and drop the message:

intercept ["presence_diff"]

def handle_out("presence_diff", _, socket), do: {:noreply, socket}
6 Likes

I use this extensively, now if only you could tell it to use a different message than presence_diff though, I tend to have more than one presence in a channel. ^.^

1 Like

You can rewrite the topic yourself fwiw by intercepting, which is probably exactly what you’re doing :slight_smile: It’s not something I’m likely to support in Presence because we would have to track the event per subscriber which would add complexity.

4 Likes

It is precisely. :wink:

2 Likes

I built an expiration mechanism for our sockets so that it made sure the user could not stay connected longer than their JWT token was valid for. I thought I’d share my experience since it may be of some use to you.

In order to do that, I had to create a GenServer which would take registrations of new sockets when they’re opened and their associated token and keep them in the state of the gen server. Periodically, the gen server would go through and look for expired tokens and if it found one and the user was still connected (i.e. the socket pid was still alive) I broadcasted a “user-1234:disconnect” message which the generated socket code shows you how to do. This disconnects all users, and the client should then attempt to reconnect. Since I denied the connection when the token is no longer valid they would not get valid connection and the after error callback would fire on the client. The client then would request a new token and reattempt connection (only once).

You may be able to do something similar, just in your case when you limit the reconnect attempts to one or more tries, you’ll have to assume then that the reason is because of too many sockets and tell the client to give up trying by doing a socket.disconnect().

3 Likes

Ok this also sounds like a very solid approach on this.
Thanks for being so descriptive, will definitely factor this into our end solution.

1 Like

Hm thats a great insight re: blocking at the socket level.
For now I’m going to add limits to the number of channels that can be joined.

Thanks for the feedback!

1 Like