Authenticating ActionCable connections - ruby-on-rails

I've found a wonderful ActionCable gem, which is a good solution for SPA.
I want to send only the html, css and js assets, all other connections will be implemented through ActionCable. It's not difficult to exchange strings or integers, but how can I login through ActionCable?

From the Readme
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
if current_user = User.find(cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end
So it looks like you could insert your own find_verified_user logic here. The reject_unauthorized_connection method lives in lib/action_cable/connection/authorization.rb for reference.
From Heroku:
[authentication] can be done in a variety of ways, as WebSockets will
pass through standard HTTP headers commonly used for authentication.
This means you could use the same authentication mechanism you’re
using for your web views on WebSocket connections as well.
Since you cannot customize WebSocket headers from JavaScript, you’re limited to
the “implicit” auth (i.e. Basic or cookies) that’s sent from the
browser. Further, it’s common to have the server that handles
WebSockets be completely separate from the one handling “normal” HTTP
requests. This can make shared authorization headers difficult or
impossible.
With this in mind it would likely be a real pain not to just use a normal web login flow to set your auth cookie, delivering your SPA after the authentication step, but hopefully this can give you some pointers.

FYI, if you have devise already installed in your application, then you can use the environment variable set by warden to find the authenticated user. For every authenticated user warden stores the user object in environment var. Every request is authenticated by warden middleware.
Note: this env is different from ENV.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user_from_env
end
private
def find_verified_user_from_env
# extracting `user` from environment var
current_user = env['warden'].user
if current_user
current_user
else
reject_unauthorized_connection
end
end
end
end
If you have not used devise then, here is another solution. Precondition is, you will have to set a signed cookie called user_id in your sessions_controller or something like that.
eg
cookies.signed[:user_id] = current_user.id
and for connection:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user_from_cookies
end
private
def find_verified_user_from_cookies
current_user = User.find_by_id(cookies.signed[:user_id])
if current_user
current_user
else
reject_unauthorized_connection
end
end
end
end

The solution is to use HTTP authorization token. It's simple, widespread and obvious. This article helped me a lot

Related

Rails Action Cable: Authorize the incoming connection from mobile devices

I want to authorize connection requests made to the websocket connection via mobile devices.
The code that I have in the Connection < ActionCable::Connection::Base is as follows:
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
if verified_user = User.find_by(id: cookies[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
This is working perfectly for the WebBrowser Clients, as I have set cookies as soon as the user signs in to the system using Devise.
That code looks something like this in the SessionsController from Devise:
class Users::SessionsController < Devise::SessionsController
# POST /resource/sign_in
def create
super
cookies[:user_id] = current_user.id
end
This is working fine.
But when I try to connect to the websocket connection via mobile, i get reject_unauthorized_connection error.
Also I tried setting cookies in the api/signin api, when the user signs in from the mobile. The api code looks something like this:
class Api::SessionsController < Api::ApiController
def create
user = User.find_by_email params[:email]
if user && user.valid_password?(params[:password])
# Set the user cookie
cookies[:user_id] = user.id
render json: { token: token_sign_in(user), user: user, department: user.department, organization: user.organization }
else
render json: { error: "invalid credentials" }, status: :unauthorized
end
end
Is the error happening because you can't set cookies on a mobile application ? If not, then what is the next best approach to authenticate the incoming websocket connection requests from mobile devices without using cookies.
When trying to authenticate connection requests from a mobile device use tokens or a unique identifiers(email id in my case) that the mobile application sends to the server while logging in, with the header.
So, these are the changes that I made in my connection.rb file.
token = request.headers[:email]
if token
#verified_user = User.find_by(email: token)
else
reject_unauthorized_connection
end
You could also use a JWT token instead of an email.

Find User for a JWT token in devise-jwt in rails

I am using devise-jwt for token based authentication in my RoR app.
The client sends in a token in the header like so: Bearer #{token}
By using authenticate_user! I am able to authenticate the user in the controller, and get the logged in user as current_user.
When I use Actioncable, the connect method in connection.rb requires to find the user from the token. The connection.rb is as follows:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
token = request.env["Authorization"]
find_verified_user
end
private
def find_verified_user
if authenticate_user!
self.current_user = current_user
else
reject_unauthorized_connection
end
end
end
end
However, the authenticate_user! is not a valid method here (as it is devise specific).
My question is how to I find the current_user here? How to identify the client who has sent the token in the header?

Using ActionCable with multiple identification methods

I develop a Ruby on Rails 5.1 application using ActionCable. User authentification via Devise works fine for several channels. Now, I want to add a second type of channels which does not require any user authentification. More precisely, I would like to enable anonymous website visitors to chat with support staff.
My current implementation of ApplicationCable::Connection for authenticated users looks like this:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
user = User.find_by(id: cookies.signed['user.id'])
return user if user
fail 'User needs to be authenticated.'
end
end
end
Anonymous users will be identified by some random UUID (SecureRandom.urlsafe_base64).
Question:
How do I best add this new type of channels? Could I add a boolean flag require_authentification somewhere, override it in my inherited channel class for anonymous communication, and switch the identification method in Connection depending on this attribute? Or would I rather have to implement a completely new module, say AnonymousApplicationCable?
Hi I came into the same problem, after looking at your solution in rails github comment, I assume it is better to create the token and keep the logic in the connect method.
So what I do was just utillize the the warden checking and if it is nil just create the anonymous token and otherwise. For this to work, I need to declare 2 identifier :uuid and :current_user
class Connection < ActionCable::Connection::Base
identified_by :current_user, :uuid
def connect
if !env['warden'].user
self.uuid = SecureRandom.urlsafe_base64
else
self.current_user = find_verified_user
end
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end

How To use devise_token_auth with ActionCable for authenticating user?

I have a Rails 5 API with devise_token_auth gem authentications.
Now I want personal chat for authenticated users. I do not have assets as I am using API and front is in native apps and I want native apps messaging.
So how I can authenticate users to use action cable for personal messaging using devise_token_auth gem
No cookies are generally supported in Rails 5 API. See: http://guides.rubyonrails.org/api_app.html#creating-a-new-application .
If you do a common HTTP-authentification at first somewhere at your site ( with devise_token_auth gem), then you get 3 auth headers - access_token, client, uid.
In such case you can use the Basic authentification for your Websockets connection (according https://devcenter.heroku.com/articles/websocket-security#authentication-authorization ) using these 3 auth headers:
Call (I use Chrome Simple WebSocket Client):
ws://localhost:3000/cable/?access-token=ZigtvvcKK7B7rsF_20bGHg&client=TccPxBrirWOO9k5fK4l_NA&uid=client1#example.com
Then process:
# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
params = request.query_parameters()
access_token = params["access-token"]
uid = params["uid"]
client = params["client"]
self.current_user = find_verified_user access_token, uid, client
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user token, uid, client_id # this checks whether a user is authenticated with devise
user = User.find_by email: uid
# http://www.rubydoc.info/gems/devise_token_auth/0.1.38/DeviseTokenAuth%2FConcerns%2FUser:valid_token%3F
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
This is similar to the common Websockets auth https://rubytutorial.io/actioncable-devise-authentication/
Such authentication is probably enough. I believe it is not necessary additionally to auth at the channel subscription and on every Websocket message sent to server:
http://guides.rubyonrails.org/action_cable_overview.html#server-side-components-connections
For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all the channel subscriptions that are created from there on. The connection itself does not deal with any specific application logic beyond authentication and authorization.
So if your connection is identified_by :current_user, you can later access current_user wherever inside your FooChannel < ApplicationCable::Channel! Example:
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearance_channel"
if current_user
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :on }
current_user.online = true
current_user.save!
end
end
def unsubscribed
if current_user
# Any cleanup needed when channel is unsubscribed
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :off }
current_user.online = false
current_user.save!
end
end
end
PS I you want to use cookies in Rails 5 API, you can switch it on:
http://guides.rubyonrails.org/api_app.html#other-middleware
config/application.rb
config.middleware.use ActionDispatch::Cookies
http://guides.rubyonrails.org/api_app.html#adding-other-modules
controllers/api/application_controller.rb
class Api::ApplicationController < ActionController::API
include ActionController::Cookies
...
Here the auth_headers set in the cookies by ng-token-auth are used to determine if the request is authenticated, if not the connection is refused.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user # this checks whether a user is authenticated with devise_token_auth
# parse cookies for values necessary for authentication
auth_headers = JSON.parse(cookies['auth_headers'])
uid_name = DeviseTokenAuth.headers_names[:'uid']
access_token_name = DeviseTokenAuth.headers_names[:'access-token']
client_name = DeviseTokenAuth.headers_names[:'client']
uid = auth_headers[uid_name]
token = auth_headers[access_token_name]
client_id = auth_headers[client_name]
user = User.find_by_uid(uid)
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
I do it this way, since our React Native app sends the authentication inside the headers:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
access_token = request.headers['access-token']
uid = request.headers['uid']
client = request.headers['client']
user = User.find_by_uid(uid)
if user && user.valid_token?(access_token, client)
logger.add_tags 'ActionCable', uid
user
else
reject_unauthorized_connection
end
end
end
end

Only connect users to ActionCable

I'm using ActinCable on my application, and I have an issue with authorization. Currently actioncable tries to authorize every single person live on the site, repeatedly as-well.
This returns a constant stream of An unauthorized connection attempt was rejectedin my log. Now that's because people visiting that aren't signed in, are also attempted to gain access.
My connection.rb looks like this:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
if current_user = User.find_by(id: cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end
now I'm wondering if I can make it so that only people that are signed in, try to become authorized by connnection.rbinstead of every visitor using the site. I am too unfamiliar with ActionCable to know how to limit this - and the documentation for ActionCable are still in their early days.
The connection attempt is when ActionCable.createConsumer() is called. You should try to call it only when user is logged in.

Resources