Real Time Rails 4: Using Server-Sent Events with Authy OneTouch

June 13, 2016
Written by
Phil Nash
Twilion

Server-Sent Events (SSE) are real-time events sent from a server and received by a browser, perfect for updating the front end when a server receives a webhook. We’re going to look at how to use SSE to implement Authy OneTouch using Rails 4.

Our end result should look a bit like this:

When logging in the site pops up a modal window that says it is waiting for OneTouch approval. The phone receives a push notification which opens up the approval. Selecting approve then causes the login screen to complete and the user to be logged in.

Authy OneTouch

Two factor authentication usually means copying a bunch of numbers from your phone to the place you’re trying to log in. Authy OneTouch is a more user friendly version of two factor authentication. As you can see above, the user just needs to accept the authentication attempt and they are logged in.

On the back end Authy sends a webhook to the application with the result of the authentication request. The application can then log the user in and choose how it updates the front end. Check out our tutorial on how to get started with Authy OneTouch and Rails for more detail on how it all works.

Real-Time Rails 4?

You might be thinking that Rails 5 is finally bringing real-time action to the Ruby world with Action Cable. If you are then you probably missed that Rails 4 included ActionController::Live, a means of streaming data from a Rails in real-time. Connect this up to the EventSource API in browsers and we have a full implementation of Server-Sent Events that we can use to send data from the server to the browser in real-time.

The tutorial for Authy that I mentioned earlier actually implements the OneTouch flow by polling the server for updates. As polling can be inefficient and result in many unnecessary requests to our server let’s replace it with SSE.

Tools

To follow this post and implement SSE in Rails you’re going to need a few things:

Once you’ve got all that we’ll get the tutorial application set up.

Getting started

The best instructions for running the tutorial are available in the project README. Follow the instructions all the way through until the application is running locally, you have enabled OneTouch on your Authy application and set the webhook endpoint to your ngrok URL.

Once you’ve followed the instructions you should be able to visit your application at http://localhost:3000 and sign up as a new user. Logout and log back in again and you will be presented with the following two factor authentication flow.

When logging in the site pops up a modal window that says it is waiting for OneTouch approval. The phone receives a push notification which opens up the approval. Selecting approve then causes the login screen to complete and the user to be logged in.

You can see in the console that the JavaScript is constantly polling our backend to see if the user is authenticated yet.

While the application waits for the user to approve the authentication request it polls the server once every 2 seconds which we see in the logs.

Let’s replace that polling mechanism with Server-Sent Events.

Adding Server-Sent Events

In order to run real-time features like SSE (and Action Cable when Rails 5 is released) it is recommended to use a threaded server, like Puma. The project currently uses Unicorn so we need to update that.

Open up the Gemfile and change

gem 'unicorn'

to

gem 'puma'

Stop your server and install the new dependency.

$ bundle install

We also need to make one change to config/environments/development.rb. At the bottom of the configure block, add:

  config.allow_concurrency = true

This allows concurrency in the development environment so that we can run our streaming endpoint and still respond to other incoming requests.

Start the server up again and you will see it is now running on Puma and has access to a number of threads. You can configure the minimum and maximum available threads if you want to, but for the purposes of this project, the default will do.

Now when you start the server it boots Puma and records the current thread configuration.

Updating the controller

Next, we need to set up a streaming endpoint on our Rails server. Enable streaming capabilities for the existing controller by opening app/controllers/authy_controller.rb and including the ActionController::Live module.

# app/controllers/authy_controller.rb

class AuthyController < ApplicationController
  include ActionController::Live

  # the rest of the controller
end

Now let’s create a test endpoint to see how it works.

Under the one_touch_status action, create a one_touch_status_live action:

# app/controllers/authy_controller.rb

class AuthyController < ApplicationController
  def one_touch_status_live
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, event: 'time')
    # stream stuff here
  end
end

We start by setting the Content-Type header to text/event-stream. Then we use the ActionController::Live::SSE class to wrap the response stream to make it easy to write events to.

For our test endpoint let’s loop and emit the current time once a second.

# app/controllers/authy_controller.rb

class AuthyController < ApplicationController
  def one_touch_status_live
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, event: 'time')
    loop do
      sse.write({ :time => Time.now })
      sleep 1
    end
  end
end

When the client disconnects the action will continue to try to send data to the client resulting in a ActionController::Live::ClientDisconnected error. We should catch that and close the stream in order to keep the error from being raised and prevent connections from leaking. We’ll put this inside an ensure block to make sure that whatever happens we close the stream.

# app/controllers/authy_controller.rb

class AuthyController < ApplicationController
  def one_touch_status_live
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, event: 'time')
    begin
      loop do
        sse.write({ :time => Time.now })
        sleep 1
      end
    rescue ClientDisconnected
    ensure
      sse.close
    end
  end
end

Let’s add a route for this endpoint. Open up config/routes.rb and add the following route:

# config/routes.rb

Rails.application.routes.draw do
  # other routes

  get "authy/status" => 'authy#one_touch_status'
  get "authy/live_status" => 'authy#one_touch_status_live'
  post "authy/send_token"

  # other routes
end

Head back to the application at http://localhost:3000, open up the dev tools console in your browser and paste in the following:

var source = new EventSource('/authy/live_status');
source.addEventListener('time', function(event) {
  console.log(event.data);
});

You will start to see JSON objects printed to the console once per second. Stop the stream by closing it in the console with:

source.close();

When you run the JavaScript in the dev tools console you see time objects printing out once per second.

Let’s update this now to push real data from the server to the front end.

Pushing real data with PostgreSQL pub-sub

In the tutorial application when a user approves or denies the authentication request, the Authy webhook hits the /authy/callback endpoint which runs the callback action in the AuthyController. This then updates the user with the status from the parameter of the webhook.

In order to push that status to the client we’d like to listen for that update whilst we have a live connection open to our streaming endpoint. Because we are using PostgreSQL in this example we can use its publish-subscribe feature. If you are using a different database without pub-sub you could achieve the same result by using something else like Redis to fill the gap.

PostgreSQL uses the keywords NOTIFY and LISTEN to publish and subscribe to events respectively. Active Record doesn’t directly support these keywords, but we can execute them using the database connection anyway.

We’ll add the notification code to the User model so open up app/models/user.rb. First, we want to send a notification when the user is saved and their authy_status has changed.

# app/models/user.rb

class User < ActiveRecord::Base
  def notify_authy_status_change
    if authy_status_changed?
      ActiveRecord::Base.connection_pool.with_connection do |connection|
        execute_query(connection, ["NOTIFY user_?, ?", id, authy_status])
      end
    end
  end
end

NOTIFY takes two arguments, a channel to notify and some text to send to that channel. We use the user id in the channel name to only send the notification to listeners subscribed to this particular user. We then send the updated status, which will come through as either “approved” or “denied”.

Because we’re using a raw connection to the database and executing SQL, I’ve actually added a couple of methods to sanitise and execute the SQL. Active Record has a santize_sql method, however it is a protected class method. I’ve made that available to instances via the clean_sql method. The private method execute_query handles sanitising and executing the SQL to reduce repetition.

# app/models/user.rb

class User < ActiveRecord::Base
  def self.clean_sql(query)
    sanitize_sql(query)
  end

  private

  def execute_query(connection, query)
    sql = self.class.clean_sql(query)
    connection.execute(sql)
  end
end

We can run the notification method every time the user model is saved using an Active Record callback.

# app/models/user.rb

class User < ActiveRecord::Base
  after_save :notify_authy_status_change
end

Now we need a method to subscribe to the user channel and listen for these notifications.

# app/models/user.rb

class User < ActiveRecord::Base
  def on_authy_status_change
    ActiveRecord::Base.connection_pool.with_connection do |connection|
      begin
        execute_query(connection, ["LISTEN user_?", id])
        connection.raw_connection.wait_for_notify do |event, pid, status|
          yield status
        end
      ensure
        execute_query(connection, ["UNLISTEN user_?", id])
      end
    end
  end
end

In this case, LISTEN only takes one argument, the channel name. We execute the SQL and then wait for the notification to arrive. When it does we yield the status. You’ll see how this works in the updated controller action below. We also ensure that we stop listening for the notification once it has arrived. Let’s see what the updated AuthyController looks like now:

# app/controllers/authy_controller.rb

class AuthyController < ApplicationController
  def one_touch_status_live
    response.headers['Content-Type'] = 'text/event-stream'
    @user = User.find(session[:pre_2fa_auth_user_id])
    sse = SSE.new(response.stream, event: "authy_status")
    begin
      @user.on_authy_status_change do |status|
        if status == "approved"
          session[:user_id] = @user.id
          session[:pre_2fa_auth_user_id] = nil
        end
        sse.write({status: status})
      end
    rescue ClientDisconnected
    ensure
      sse.close
    end
  end
end

This time, we select the user using the temporary session store of their ID. We then subscribe to changes to their authy_status and when we receive a change, push it to the front end. If the status is “approved” we also log the user in finally. As you can see here, we use a block with on_authy_status_change and when the status is yielded in the user method, it is passed into the block.

All we need to do now is update our JavaScript to stop polling and start listening for updates.

Updating the JavaScript

Open up app/assets/javascripts/sessions.js and replace the code within the checkForOneTouch function with the following:

// app/assets/javascripts/sessions.js

  var checkForOneTouch = function() {
    var source = new EventSource("/authy/live_status")
    source.addEventListener("authy_status", function(event) {
      var data = JSON.parse(event.data);
      if (data.status === "approved") {
        source.close();
        window.location.href = "/account";
      } else if (data.status === "denied") {
        showTokenForm();
        triggerSMSToken();
      }
    })
  };

We’re listening to our SSE for the “authy_status” event. When it arrives we parse the JSON and if the event is approved we close the stream and redirect the user to their account page. Otherwise we use the existing behaviour, show the token form and trigger an SMS.

Restart the server and go through the login process again (make sure ngrok is still running otherwise you won’t get your webhook).

We run the same authentication flow as before, however this time we see that there are no polling events in the log. We have successfully implemented Server-Sent Events.

Hooray! Real-time updates to the front end, no polling required. Check out my fork of the original tutorial to see the complete changes.

Rails 4 does Real-Time

Rails 5 and Action Cable might not be released just yet, but Rails 4 can still perform some pretty sweet real-time manoeuvres. This works really well for server based updates as we’ve seen with Authy OneTouch. It would also work for live updating call statuses on a dashboard like Sam showed using Socket.io and React or creating a dashboard for conference events like Dominik did using Socket.io and Angular.

Server-Sent Events just provide one-way real-time events though. Action Cable will bring two-way real-time streams to Rails for more complicated interactions, like a chat room. Of course, you don’t have to build all that complexity yourself if you use IP Messaging ;)

Do you like the look of SSE in Rails or are you going to be holding out for Action Cable? Let me know in the comments below, give me a shout on Twitter at @philnash or drop me an email at philnash@twilio.com.