How to create user specific channels in a Rails API that uses Devise

by Dario Bogović, October 24, 2022

(Or how not to send secret Devise keys through channels and instead set it up properly)

 
For everyone looking for a quick solution, here’s what this blog post is about: making ActionCable channels for verified users inside a Rails API. 
First, let’s get the TL;DR out of the way so you can be on your way to getting your API functioning. If you are making a Rails API you won’t be able to track cookies or sessions. That means that the frontend side of your app will have to send you a public key by which you can find the user in the database. You do so by sending the JWT token, decoding it and using it in a User.find_by_jti(decoded_token.first['jti']) call to the database. After saving that value to a current_user variable, you will be able to create an ActionCable channel for the specific user (e.g. User_channel_#{current_user}) and broadcast on it. 
 
And now, let’s take you step by step through this issue and how we tackled it. 

WebSockets (ActionCable)

 
WebSockets have an undeniable role in the user experience while using a web application. Without the WebSockets protocol, most real-time communication within a given application would be impossible (that is inside a single UTP connection). In Rails terms, ActionCable would be considered the main culprit for integrating the protocol. As with most of Rails, the “Rails magic” part takes care of most of the “dirty” stuff and leaves everything somewhere under the hood requiring us to provide only the main information for the setup and usage and, later, bear the fruits of Rails’ hard work. 
But sometimes Rails in and of itself isn’t enough and we need to incorporate gems to make our lives even easier. Devise is such a gem. It allows us to authenticate users and encrypt their personal data (such as passwords). Quite frankly, I find it surprising that it isn’t a built in feature preexisting inside Rails applications… But I digress. 
 
Before we dig into the meat and potatoes of this blog post, we need to understand what might not be possible when you’re trying to send a message to the current user. 
First, we need to create a channel specific to each user. Why? Well, if we send all the information to everyone, we’ll slow down our application needlessly, also we might send potentially sensitive information to ears that shouldn’t hear them. To be more precise, perhaps you’re building an online store application and you’d want the shopping cart icon to be automatically updated when the current user adds something to the cart. Why would you send that information to all users? Other users don’t need to know what this one is purchasing. And just imagine what could happen if the application also processes invoicing data. 
So, now we have created a simple user channel for each user and everything’s going to work like a charm! Unfortunately no. The current_user isn’t available outside of the ApplicationController scope, and as ApplicationCable doesn’t inherit from it, the current user won’t be available.

# user_channel.rb inside the channels folder

class UserChannel < ApplicationCable::Channel
  def subscribed
    stream_for "user_channel_#{current_user.id}"
  end
end
 
And even if it were, this wouldn’t be a good solution! Here’s why: 
The Rails app we are building is an API to which the frontend connects. And if someone has bad intentions, he or she (or undefined) might try to connect to a WebSocket manually through the browser console. The only thing they would need to do is make an educated guess as to what the channels are called, and at that point, all the data that is being streamed to that channel would be available to our ‘hacker’. 
 
Let’s verify the user! 

User verification (Devise)


Many online sources provide different solutions for this problem. We are going to use the data that is already provided to us, so there is no need for additional gems, dependencies, or anything of that sort. 
But first, let’s make things a bit more difficult: let’s say we ARE creating an API. That means that the sessions and cookies arrays aren’t available to us. (Yes, User.find_by(id: session[‘user_id’])) would be such a nice solution, wouldn’t it? We’re going to do something similar). 

This means that the frontend of our application needs to provide us with something to find the user by. But we still have to be careful about what we are sending. There may be ears listening to anything and everything we do, so let’s just give them (and us) only the data we already have. 
Devise uses JSON Web Tokens (JWT in further text) for encrypting data. 

JWT is a type of RSA public key cryptosystem. It’s up to the curious reader to do some research and ‘light’ reading to get the exact meaning of all of this because it’s out of the scope of this blog. 
In layman’s terms: the data is encrypted using two numbers: a public and a private key. And here is the point in which we have to be careful: we can’t just send private keys around all willy-nilly. But we can share the public keys. In some public key cryptosystems, the public key is used as the address of the user. 
We are going to get the public key from the frontend. It is accessible there because that is the place the request comes from, and it has to have a public key attached to it. Decoding that token will provide us with the JTI token which uniquely identifies the JWT, and with that, the current user. 

# connection.rb
require 'jwt'
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      token = request.params[:jwt].split(' ').last
      decoded_token = JWT.decode(token, ENV['DEVISE_JWT_SECRET_KEY'])
      if (current_user = User.find_by_jti(decoded_token.first['jti']))
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end


And voila! We have created a system that allows us to send information only to verified users while being especially careful not to provide any data of the users through our channels. 


Thanks for reading so much!

I shall leave you with the wise words that Sifu Gandalf provided young Harry Potter just before being beamed up to the starship Enterprise and entering Narnia: I choose you and may the Force be ever in your favour, mate!

P.S If you do not know how to integrate Devise with Rails API-based application, my colleague wrote a blog about it. He will guide you step-by-step on how to integrate Devise with Rails API-based application. Here is the link:
https://thespian.hr/blog/devise-authentication-for-the-rails-7-api-application
About the author:
Author avatar
Dario Bogović
Thespian employee
Scout (the trailblazing and pathfinding kind)