Authentication

How to secure your StimulusReflex application

If you're just trying to bootstrap a proof-of-concept application on your local workstation, you don't technically have to worry about giving ActionCable the ability to distinguish between multiple concurrent users. However, the moment you deploy to a host with more than one person accessing your app, you'll find that you're sharing a session and seeing other people's updates. That isn't what most developers have in mind.

Authentication != Authorization

Libraries like Pundit, CanCanCan and Authz don't work on reflexes and might justify keeping state mutations with destructive outcomes in the controller.

Another valid approach to authorization is to make use of the before_reflex callbacks, where you could potentially call throw :abort if the user is acting out above their pay grade.

Encrypted Session Cookies

You can use your default Rails encrypted cookie-based sessions to isolate your users into their own sessions. This works great even if your application doesn't have a login system.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_action_cable_identifier
private
def set_action_cable_identifier
cookies.encrypted[:session_id] = session.id.to_s
end
end
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :session_id
def connect
self.session_id = cookies.encrypted[:session_id]
end
end
end

User-based Authentication

Many Rails apps use the current_user convention or more recently, the Current object to provide a global user context. This gives access to the user scope from almost all parts of your application.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_action_cable_identifier
private
def set_action_cable_identifier
cookies.encrypted[:user_id] = current_user&.id
end
end
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
user_id = cookies.encrypted[:user_id]
return reject_unauthorized_connection if user_id.nil?
user = User.find_by(id: user_id)
return reject_unauthorized_connection if user.nil?
self.current_user = user
end
end
end

Note that without intervention, your Reflex classes will not be able to see current_user. This is easily fixed by setting self.current_user = user above and then delegating current_user to your ActionCable Connection:

app/reflexes/example_reflex.rb
class ExampleReflex < StimulusReflex::Reflex
delegate :current_user, to: :connection
def do_stuff
current_user.first_name
end
end

Devise-based Authentication

If you're using the versatile Devise authentication library, your configuration is even easier.

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 = env["warden"].user
current_user
else
reject_unauthorized_connection
end
end
end
end

Now you're free to delegate current_user. Be home by lunch:

app/reflexes/example_reflex.rb
class ExampleReflex < StimulusReflex::Reflex
delegate :current_user, to: :connection
end