Tracking Anonymous/Unauthorized Users with Elixir/Phoenix Channels and Presence

OBJECTIVE:

  • We want authorized users to be able to read/write over a socket
  • We want unauthorized (anonymous) users to be able to view what’s happening in a given room but not participate.
  • Display authorized users in a list, one per row.
  • Display how many “Anonymous” users are in the room but since they are less interesting, instead of displaying each one on it’s own line, we want to combine them into one line item, as a count (i.e. “Anonymous: 23”).

DISCLAIMER: I’m assuming you know how to use Phoenix.Channels and Phoenix.Presence the normal way. I will omit some code that may be necessary to get Presence working. I am using Guardian/Ueberauth for authentication (I used this example project to implement it: https://github.com/hassox/phoenix_guardian). I’m also fairly new to Elixir/Phoenix, so this may not be best practice, let me know if there’s something that can be improved.

Here’s how I did it. I’m sure there are plenty of ways to improve it (let me know), but it works.

SERVER SIDE

def join(“rooms:” <> id, %{“guardian_token” => token}, socket) do
socket = case sign_in(socket, token) do
{:ok, authed_socket, _guardian_params} -> authed_socket
{:error, reason} -> socket
end
case Repo.get(Room, id) do
%Room{} = room ->
send(self, :after_join)
user = current_resource(socket) || %User{}
socket = assign(socket, :user, user)
{:ok, socket}
nil -> {:error, %{reason: “Room does not exist”}}
end
end

Here we check for two things. Is a User present, and is the requested Room valid. The first section is straight from Guardian/Ueberauth. If the token is present, sign_in/2 will add stuff to the socket to allow us to grab the user later, if not, we just keep the original socket. We then check that the requested Room exists, if so, we send ourselves an :after_join message (explained below). And assign a User to the socket. ```current_resource/1``` is from Guardian, it will return a User or nil. I like to stick an empty User in the socket instead of nil, because this allows us to treat it like a User, and access fields(user.id, user.username, etc.) without having to check if it’s a valid User or nil.

Next we want to block these “Anonymous” users from pushing stuff into the socket (obviously some client-side checks should be in place too).

def handle_in(_, _, %{assigns: %{user: %User{id: nil}}} = socket) do
{:reply, {:error, %{reason: “Not signed in”}}, socket}
end

Here we just match any inbound messages from anon users, and reject them. There may be situations where you want to accept some stuff from anon users, so you’ll have to do something else here.

Let’s look at handling Presence in the :after_join

def handle_info(:after_join, socket) do
Presence.track(socket, socket.assigns.user.id, %{
online_at: :os.system_time(:milli_seconds),
username: socket.assigns.user.username
})
  push socket, “presence_state”, Presence.list(socket)
{:noreply, socket}
end

Pretty straight forward. You can add other stuff here too, like sending the room’s current messages state, etc. I’ve stripped that stuff out for the example, but here’s a good place to do that.

Since we have a empty User struct (%User{}) instead of just nil, we can call user.id, and user.username for all sockets, the signed in users will have valid values, the anon users will be nil.

The way this gets represented by Presence is as follows (2 authorized users, 2 anon users):

%{ 
   “” => %{metas: [
%{username: nil, online_at: 123, phx_ref: “abc”},
%{username: nil, online_at: 123, phx_ref: “def”}
]},
   “11” => %{metas: [
%{username: “test_user”, online_at: 123, phx_ref: “ghi”}
]},
   “12” => %{metas: [
%{username: “test_user2”, online_at: 123, phx_ref: “jkl”}
]}
}

Regular users will have a id key, and an array with one item (it can be longer if the user is using the service from multiple sources). And the empty key (anon users) will have one entry for each anon user. There may be a reason you don’t want this to be an empty string (I’ve had no issues with it), in which case you can either check the user’s id before adding it to Presence, or initialize the anon users as %User{id: :anonymous}

That covers everything from the server side. Let’s look at the client side.

CLIENT SIDE

listBy(user_id, {metas: metas}) {
if (!user_id) {
return {
username: “Anonymous”,
count: metas.length
}
}
return {
username: metas[0].username,
}
}
renderAll() {
var allUsers = Presence.list(this.presences, this.listBy);
var anonymousUsers = $.grep(allUsers, function(item){
return item.username == “Anonymous”;
})[0] || {username: “Anonymous”, count: 0};
var authorizedUsers = $.grep(allUsers, function(item){
return item != anonymousUsers
});
this.$listEl.html(
authorizedUsers.map(presence => `
<li>${presence.username}</li>
`).join(“”) + `
<li>${anonymousUsers.username}: ${anonymousUsers.count}</li>
`)
}

This is pretty straight forward. In our listBy method, we check to see if the user_id is present (indicating a authorized user), if it’s not, then we count the metas array, if it is present, we grab the fields we want to display.

We then grep the list and grab the anonymous users, grep again to grab the authorized users, then we render the HTML and stick it in the DOM.

It should output like this:

  • test_user
  • test_user2
  • Anonymous: 2