I wanted to add real-time notification support to my website. Well not just my website but all my other native applications (Desktop/iPhone/Android). I also needed to secure it with user authentication and authorization.
I started with a single Ruby on Rails application. Rails has convenient built-in WebSocket support via Action Cable. One problem with Action Cable is that it doesn’t scale. There is a good article describing the pros and cons of Action Cable here: https://www.ably.io/blog/rails-actioncable-the-good-and-the-bad
At LayerKeep we are using a micro-service architecture where we have different services written in different languages. Currently our inter-service communication is done via RabbitMQ.
I think RabbitMQ is awesome and when it comes to routing messages, it’s the best. It’s also built on top of Erlang so it’s very fast. It has a plugin architecture that allows you to enable many different features. And it’s very simple to use. Since we are already using RabbitMQ for our inter-service communication I thought it would be nice to be able to use the same backend for handling our WebSocket connections.
Rabbit has a nice plugin just for that using Web STOMP (https://www.rabbitmq.com/web-stomp.html)
(* You can also use MQTT protocol instead of STOMP using https://www.rabbitmq.com/web-mqtt.html) .
Since this plugin ships in the core distribution, simply enable it with:
rabbitmq-plugins enable rabbitmq_web_stomp
Depending on how you configure stomp you could connect directly to it. I personally like to proxy my requests through nginx. If you are using nginx then something as simple as a location path like this should work.
location /ws { proxy_set_header Host $http_host; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass http://your_rabbitmq_host:15674; }
Then for you website you can find a nice javascript client to connect with stomp. I personally use https://www.npmjs.com/package/webstomp-client. Seems to work pretty well. I’m also using React so I wrapped it in another component to handle all the connection and subscription events. The nice thing about having the proxy is I don’t have to worry about changing the url or port.
import webstomp from 'webstomp-client'; setupWebSocket() { var protocol = document.location.protocol == "https:" ? 'wss' : 'ws'; var wspath = `${protocol}://${document.location.hostname}/ws` var client = webstomp.client(wspath, {debug: false}); client.ws.onopen = this.onOpen this.setState( { client: client }) }
If you just want to receive transient notifications while connected, you can connect using a temporary queue. As soon as the connection is disconnected the queue will be removed. This means that if a notification is sent when a user refreshes the page the message wont be sent to the user.
You can also make the queue permanent so if messages come in while the user isn’t connected they will be persisted and as soon as the user connects they will receive all the previous messages.
What about Authentication/Authorization?
I really liked the idea of using Rabbit but needed to figure out how to handle authentication and authorization.
RabbitMQ has a default internal user/password authentication that uses its own internal user database. We don’t want to have to manage all ours users in multiple places just for queues so we need something dynamic. Luckily there is a RabbitMQ plugin for handling auth using http (https://github.com/rabbitmq/rabbitmq-auth-backend-http).
When you try to connect to Rabbit with a username/password it will make a request to your backend server to authenticate that username/password. This is great but I don’t want the user to have to enter their username and password again. I figured authenticating using an OAuth token could work well.
RabbitMQ has auth_backends that it will try in order. For example:
auth_backends.1.authn = internal auth_backends.1.authz = rabbit_auth_backend_ip_range auth_backends.2 = rabbit_auth_backend_http auth_http.http_method = post auth_http.user_path = http://auth_api/auth/user auth_http.vhost_path = http://auth_api/auth/vhost auth_http.resource_path = http://auth_api/auth/resource auth_http.topic_path = http://auth_api/auth/topic
post '/auth/:kind', to: 'rabbit#auth'
def auth // Get the user user = User.find_by(username: params["username"]) render status: :ok, json: "deny" and return unless user // First call will be to /auth/user if params["kind"] == "user" token = user.access_tokens.where(token: params["password"]).first if token.nil? || !token.accessible? permission = "deny" end else /*** *** Handle all the other authorizations your app *** requires something like ***/ if params["resource"] == "exchange" and params["permission"] != "read" permission = "deny" end end render status: :ok, json: permission and return end
Look at the http backend plugin for all the other parameters.
You can find a docker image for RabbitMQ with http auth config here: https://github.com/frenzylabs/rabbitmq.