At Opendoor, we're growing our application to take the pain out of Real Estate. Like most places we have a front end that our customers interact with, and a back-end system that our operations staff use.

We wanted an authenticated messaging service to publish events from our Rails application to our browser page in near realtime. Our front-end is a long-lived Angular application and when we deploy a new version of the site, we want our clients to refresh. Our back-end is a Rails app.

We like Rails – it lets us move quickly – but this is an area where it’s showing its age.

Goals

  • Start simple. At first we just need to notify our app that it should take steps to reload on a new deploy.
  • Have room to grow. There are so many places we could end up using this functionality. For example, adding a notifications ticker on our backend.
  • Secured communications. We want to be able to use this for admins, logged-in customers and public messaging.

Before we get too far… Yes, there are many services out there that provide this kind of functionality, and yes we could have used one of those.

We talked about using Elixir and Phoenix. Some of us have been toying with Phoenix for a while and this felt like a great way to introduce it to our engineering team. Elixir and Phoenix fit this style of problem really well. First class support for near real-time communication and push to the browser made it trivial to add to our arsenal. The tight focus of our initial requirements gave us a decent sandbox, so @nolman whipped up a Phoenix app over a weekend. The simplicity of it, and speed at which we were able to get up and running was great.

We also want some authentication between Rails and Phoenix when our page tries to connect to the channel. Our initial use case of notifying of a new deploy doesn't need authentication, but other events that we want require it. The tricky part of it is that our Rails application knows about authenticated sessions, but how to authenticate those logged in people with our Phoenix channels?

Enter Guardian, a new authentication library based on JSON Web Tokens. Guardian’s authentication handles normal web endpoints and also channels.

The Plan

  1. Have the Rails application generate a JWT (restricted via CSRF) on page load.
  2. When connecting to the channel, supply the Rails minted JWT for authentication.
  3. Profit

It looks like this:

phoenix-on-rails-schematic

Rails setup

Let’s start with the Rails side. When we’re going to talk to a Phoenix channel we need to include the phoenix.js file into the asset pipeline and then join the channel on the client side. We use Angular, so there’s a bit of that mixed up in our JS.

//= require ../module
//= require phoenix
(function() {
var socket = new Phoenix.Socket("<%= Conf.load('pusher.yml')['ws_base_url'] %>/events");
socket.connect();

OC.factory('Pusher', function($document) {
  var csrfToken, guardianToken, csrfMeta, guardianMeta;

  csrfMeta = _.find($document.find('meta'), 'name', 'guardian-csrf');
  guardianMeta = _.find($document.find('meta'), 'name', 'guardian-token');

  if (csrfMeta) csrfToken = csrfMeta.content;
  if (guardianMeta) guardianToken = guardianMeta.content;

  return {
    join: function(channelName) {
      var chan = socket.chan(channelName, { guardian_token: guardianToken, csrf_token: csrfToken });
      chan.join();
      return chan;
    }
  };
});
})();

A config yaml file has our URLs in it per environment, and application.rb has our guardian key. This is the secret we share between Rails and Phoenix so we can generate/verify our JWTs on each system.

# config/application.rb

config.guardian_secret = ENV['GUARDIAN_SECRET'] || Rails.env

The javascript will find our CSRF and JWT and create a channel with the correct tokens for us to authenticate.

Generating the JWT in Rails

Next up, let’s generate the JWT from the Rails application and include it in the page. We want to use the :csrf type of token for our JWT, so it's only useful when you're visiting a page we generated. (This is the most complicated part in our Rails/Phoenix mashup and most of it is constructing a hash.)

# app/helpers/guardian_helper.rb

module GuardianHelper
  ISSUER = "od-web-#{Rails.env}"
  DIGEST = OpenSSL::Digest.new('sha256')

  def guardian_token_tags
    token = Base64.urlsafe_encode64(SecureRandom.random_bytes(32))
    [
      "<meta content="#{jwt(token)}" />",
      "<meta content="#{token}" />",
    ].shuffle.join.html_safe
  end

  private

  def jwt(token)
    JWT.encode(jwt_claims(token), Rails.configuration.guardian_secret, 'HS256')
  end

  def jwt_claims(token)
    {
      aud: :csrf,
      sub: jwt_sub,
      iss: ISSUER,
      iat: Time.now.utc.to_i,
      exp: (Time.now + 30.days).utc.to_i,
      s_csrf: guardian_signed_token(token),
      listen: jwt_listens,
      publish: jwt_publish,
    }
  end

  def jwt_sub
    return {} unless current_human.present?
    {
      id: current_human.id,
      name: current_human.full_name,
      email: current_human.email,
      crews: current_human.crews.map(&:identifier),
    }
  end

  def jwt_listens
    listens = ['deploys:web', 'public:*']
    listens.push('private:*') if current_human.try(:in_crew?, :admins)
    listens
  end

  def jwt_publish
    publish = ['public:*']
    publish.push('private:*') if current_human.try(:in_crew?, :admins)
    publish
  end

  def guardian_signed_token(token)
    key = Rails.configuration.guardian_secret
    signed_token = OpenSSL::HMAC.digest(DIGEST, key, token)
    Base64.urlsafe_encode64(signed_token).gsub(/={1,}$/, '')
  end
end

There's a bit going on there. First we're going to put our token and JWT into the head in meta tags. When we generate the JWT claims, the standard fields are there. For those who are unfamiliar:

  • aud - Audience. We're setting it to csrf so that csrf is verified as part of verifying the JWT.
  • sub - The authenticated resource. In our case, we're just using a Hash with id, name, email and an array of 'crews' (crews are just groups)
  • iss - Which system issued the token
  • iat - Issued at (utc timestamp)
  • exp - The time that the token should expire (utc timestamp)

They're the standard ones, but JWTs encode 'claims'. That is, the issuer of the JWT asserts that the 'claims' are correct. If you trust the issuer, you trust the claims (which can be anything).

A quick side note, try not to put too many claims into your JWT. It has to fit into the Authorization header for HTTP requests.

The other 'claims' that we're asserting in this token are:

  • listen - The channel topics that you're allowed to listen to
  • publish - The channel topics you're allowed to publish to
  • s_csrf - This is a Guardian claim. It's the signature (HMAC SHA256) of the CSRF token. When you make this claim you must supply the original token with each request, and the SHA256 signed with the shared secret in the s_csrf claim.

We're allowing everyone to listen to "deploys:web", and "public:*". Admins can also listen to "private:*". Publish is similar, but no one can publish to "deploys'. To get this on our page we just call the guardian_token_tags method in our layout.

We had to generate the token ourselves for this, not rely on the actual CSRF token in the session on Rails’ side. This is because Rails masks its CSRF token so that it's different every time you look at it. As a future enhancement we'll try to get this happening.

Bear in mind we're sharing the Guardian secret between our Rails application and our Phoenix app.

Phoenix

That's it for the Rails side. Most of the code was to get the claims we want to use in Phoenix.

The implementation of the Phoenix app itself is pretty trivial: A controller we can post to with messages to, and the channel to distribute those messages to the right people.

Controller

# web/controllers/publish_controller.ex

defmodule Pusher.PublishController do
  use Pusher.Web, :controller

  plug :authenticate

  def publish(conn, params) do
    topic = params["topic"]
    event = params["event"]
    message = params["payload"] || %{}
    Pusher.Endpoint.broadcast! topic, event, message
    json conn, %{}
  end

  defp authenticate(conn, _) do
    secret = Application.get_env(:pusher, :authentication)[:secret]
    if Plug.Crypto.secure_compare(hd(get_req_header(conn, "authorization")), secret) do
      conn
    else
      conn |> send_resp(401, "") |> halt
    end
  end
end

We have some basic shared secret authentication we use to let the Rails application POST to our Phoenix application, take a JSON payload, and publish it to the relevant channel. In the future, we’ll upgrade this code to use an application JWT.

Config

Setting up Guardian on our Pheonix application was easy. Just follow the README, implement the serializer, and you’re pretty much done. The only extra thing we had to do was pre-declare our custom claims as atoms.

# config/config.exs

config :joken, config_module: Guardian.JWT

config :guardian, Guardian,
  issuer: "Pusher",
  ttl: { 30, :days },
  verify_issuer: false,
  serializer: Pusher.GuardianSerializer,
  atoms: [:listen, :publish, :crews, :email, :name, :id]

As a protection, Joken will only convert strings into existing atoms so we have to pre-declare them.

For the uninitiated, Joken is the library that looks after encoding and decoding JWT in Elixir.

For this simple app, possession of the JWT is enough. We don't yet have a need to call back to Rails, so we just store the relevant data right in the token itself (id, email, name etc)

Serializer

Our Guardian serializer takes what it finds in the subject field and uses that as the authenticated 'resource'. In our case, a simple map of attributes.

# lib/pusher/guardian_serializer.ex

defmodule Pusher.GuardianSerializer do
  @behaviour Guardian.Serializer

  def for_token(data), do: { :ok, data }
  def from_token(data), do: { :ok, data }
end
Channel

The only thing left to do is get a channel together.

# web/channels/event_channel.ex

defmodule Pusher.EventChannel do
  use Phoenix.Channel
  use Guardian.Channel

  # no auth is needed for public topics
  def join("public:" <> _topic_id, _auth_msg, socket) do
    {:ok, socket}
  end

  def join(topic, %{ claims: claims, resource: _resource }, socket) do
    if permitted_topic?(claims[:listen], topic) do
      { :ok, %{ message: "Joined" }, socket }
    else
      { :error, :authentication_required }
    end
  end

  def join(_room, _payload, _socket) do
    { :error, :authentication_required }
  end

  def handle_in("msg", payload, socket = %{ topic: "public:" <> _ }) do
    broadcast socket, "msg", payload
    { :noreply, socket }
  end

  def handle_in("msg", payload, socket) do
    claims = Guardian.Channel.claims(socket)
    if permitted_topic?(claims[:publish], socket.topic) do
      broadcast socket, "msg", payload
      { :noreply, socket }
    else
      { :reply, :error, socket }
    end
  end

  def permitted_topic?(nil, _), do: false
  def permitted_topic?([], _), do: false

  def permitted_topic?(permitted_topics, topic) do
    matches = fn permitted_topic ->
      pattern = String.replace(permitted_topic, ":*", ":.*")
      Regex.match?(~r/\A#{pattern}\z/, topic)
    end
    Enum.any?(permitted_topics, matches)
  end
end

There's a bit of text in the channel, but it's all pretty straightforward.

With this setup, we have an authenticated live server push from Rails to our Javascript application, and we can continue to grow it to make it more useful. Phoenix has been a lot of fun to hack on, and the fact that we can make use of its features from our Rails application is great!

If you're interested in working with our team on any of this, we're currently hiring!