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.

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
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_suff
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