Phoenix’ Channels With Coherence authentication

An article, posted almost 6 years ago filed in coherence, elixer, erlang, vm, authentication, Phoenix, framework, fast, messaging, rails, ActionCable & websockets.

I started exploring Phoenix for one thing only: Channels (or actually fast real time communication over websockets). In this post I explore how to use them (yes this is a follow up of My first Phoenix-app-post).

Phoenix’ Channels

Preparing for the authentication problem

Websockets don’t pass session cookies. Because we don’t have access to these we need to transfer the user’s identity in a different way. One of the recommendations I found was passing a user_token using a <meta>-tag (adjusting templates/layout/app.html.eex):

<%= if Coherence.current_user(@conn) do %>
<%= tag :meta, name: "user_token", content: Phoenix.Token.sign(@conn, "user", Coherence.current_user(@conn).id) %>
<% end %>

We can access this with a simple query selector in javascript:

document.querySelector("meta[name=user_token]").content

But that’s for later. Let’s move to the server side, since we need something to connect to, a Socket.

Socket

In our default project there is already a user_socket.ex in the web/channels folder. Open it and uncomment the following line:

channel "room:*", MyApp.RoomChannel

Then rewrite the connect function to accept a token as a parameter:

def connect(%{"token" => token}, socket) do
  case Phoenix.Token.verify(socket, "user", token, max_age: 1209600) do
    {:ok, user_id} ->
      socket = assign(socket, :user, MyApp.Repo.get!(MyApp.User, user_id))
      {:ok, socket}
    {:error, _} ->
      :error
  end
end

Phoenix.Token.verify does all the magic, and turns a given token into a user_id, allowing us to access the user through Repo.get! (namespaced by the application name here).

Note As said, I really found this something to get used to: Repo.get!(User, id), instead of User.find(id)

Above code is also mentioning a RoomChannel, which we will create next:

defmodule MyApp.RoomChannel do
  use Phoenix.Channel
  # intercept ["new_post"]
  
  def join("room:" <> _private_room_id, _params, socket) do
    {:ok, socket}
  end

  def handle_in("new_post", %{"message" => message}, socket) do
    user = Coherence.current_user(socket)
    changeset = MyApp.Post.changeset(%MyApp.Post{user: user}, %{message: message)
    {status, post} = MyApp.Repo.insert(changeset)
    if status == :ok do
      broadcast! socket, "new_post", %{message: post.message, id: post.id, inserted_at: post.inserted_at}
    end
    {:noreply, socket}
  end
  
  # def handle_out("new_post", payload, socket) do
  #  user = socket.assigns[:user] 
  #  if MyApp.User.mentioned?(payload.message, user) do
  #    push socket, "new_post", payload
  #  end
  #  {:noreply, socket}
  # end
end

I’ve commented out the handle_out/3 code here (note /3 depicts that this function has 3 arguments.

When a message enters the room channel the user is retrieved from the socket, a changeset is prepared (like in our controller) and if ok a new message is broadcast in the room.

Note: This message will flow through handle_out when it matches the intercept, which allows us to do some checking. In this case the code calls the User.mentioned?/2-function, which checks whether the logged in user-name is mentioned in the message and only pushes the message to the user when the user is mentioned.

Plugging into the socket, client-side

Foundation comes with the necessary code. Simply uncomment the following line in app.js:

import socket from "./socket"

And update the code to something along the lines of the following:

import {Socket} from "phoenix"

let socket = null

if (document.querySelector("meta[name=user_token]")) {

  socket = new Socket("/socket", {
    params: {
      token: 
      document.querySelector("meta[name=user_token]").content 
    }
  })

  socket.connect()

  // Now that you are connected, you can join channels with a topic:

  let channel = socket.channel("room:1", {})

  let message = document.querySelector("#post_message")
  let messagesContainer = document.querySelector("#posts")

  message.addEventListener("keypress", event => {
    if(event.keyCode === 13){
      channel.push("new_post", {message: message.value})
      message.value = null
    }
  })
  
  channel.on("new_post", post => {
    let messageItem = document.createElement("li");
    messageItem = post.message
    messagesContainer.appendChild(messageItem)
  })

  channel.join()
    .receive("ok", resp => { console.log("Joined successfully", resp) })
    .receive("error", resp => { console.log("Unable to join", resp) })
}

export default socket

I made sure I only connect to a socket when a user is actually logged in (when a token is mentioned in the header, this to prevent unneeded traffic).

The listener channel.on(“new_post”…) displays any messages sent to the room tagged new_post. The rest is straight forward JavaScript (ES6-style).

We’re relying here on some more dom code, but make sure the page you’re displaying the posts on contains an input field with id post_message. And a container to be filled with posts with id posts (in this case a <ol>), but I’ll leave the very HTML up to you.

Conclusion

After two days of experimenting I’m confident building an actual application in Phoenix / Elixir. While the programming style is something to get used to, its response is fast. There are quite some things I really like about the framework, including the schema definition inclusion and valid parameter parsing in the model. I recommend you to try Phoenix yourself!

Op de hoogte blijven?

Maandelijks maak ik een selectie artikelen en zorg ik voor wat extra context bij de meer technische stukken. Schrijf je hieronder in:

Mailfrequentie = 1x per maand. Je privacy wordt serieus genomen: de mailinglijst bestaat alleen op onze servers.