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.
Since StimulusReflex v3.4, there is now an additional concept that you should understand - Tab Isolation - which is adjacent to but not the same as authentication. Authentication is about who sees what, while Tab Isolation is about what you see if you open the same thing, twice.

Authentication Schemes

Encrypted Session Cookies

You can use your Rails session to isolate your users so that they don't see each other's updates. This works great even if your application doesn't have a login system.
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :session_id
4
5
def connect
6
self.session_id = request.session.id
7
reject_unauthorized_connection unless session_id
8
end
9
end
10
end
Copied!

Current User

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
1
class ApplicationController < ActionController::Base
2
before_action :set_action_cable_identifier
3
4
private
5
6
def set_action_cable_identifier
7
cookies.encrypted[:user_id] = current_user&.id
8
end
9
end
Copied!
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
5
def connect
6
user_id = cookies.encrypted[:user_id]
7
return reject_unauthorized_connection if user_id.nil?
8
user = User.find_by(id: user_id)
9
return reject_unauthorized_connection if user.nil?
10
self.current_user = user
11
end
12
end
13
end
Copied!
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
1
class ExampleReflex < StimulusReflex::Reflex
2
delegate :current_user, to: :connection
3
4
def do_stuff
5
current_user.first_name
6
end
7
end
Copied!

Devise

If you're using the versatile Devise authentication library, your configuration is even easier.
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
5
def connect
6
self.current_user = find_verified_user
7
end
8
9
protected
10
11
def find_verified_user
12
if (current_user = env["warden"].user)
13
current_user
14
else
15
reject_unauthorized_connection
16
end
17
end
18
19
end
20
end
Copied!
If you have multiple Devise user models, you need to specify env["warden"].user(:user) or the variable will return nil.
Delegate current_user to the ActionCable connection and be home by lunch:
app/reflexes/example_reflex.rb
1
class ExampleReflex < StimulusReflex::Reflex
2
delegate :current_user, to: :connection
3
end
Copied!

Sorcery

If you're using Sorcery for authentication, you'll need to pull the user's id out of the session store.
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
5
def connect
6
self.current_user = User.find_by(id: request.session.fetch("user_id", nil)) || reject_unauthorized_connection
7
end
8
end
9
end
Copied!
Now you're free to delegate current_user to the ActionCable connection.
app/reflexes/example_reflex.rb
1
class ExampleReflex < ApplicationReflex
2
delegate :current_user, to: :connection
3
end
Copied!

Tokens (Subscription-based)

You can clone a simple but fully functioning example application based on the Stimulus Reflex Harness. It uses Devise with the devise-jwt gem to create a JWT token which is injected into the HEAD. You can use it as a reference for all of the instructions below.
There are scenarios where developers might wish to use JWT or some other form of authenticated programmatic access to an application using websockets. For example, you can configure a GraphQL service to accept queries over ActionCable instead of providing an URL endpoint for traditional Ajax calls. You also might need to support multiple custom domains with one ActionCable endpoint. You might also need a solution that doesn't depend on cookies, such as when you want to deploy multiple AnyCable nodes on a service like Heroku.
Your first instinct might be to authenticate in connection.rb using ugly hacks where you pass a token as part of your ActionCable connection URL. While this seems to make sense - after all, this is close to how the other techniques above work - putting your token into the URL is a real security vulnerability and there's a better way: move the responsibility for authentication from the ActionCable connection down to the channels themselves. Let's consider a potential solution that uses the Warden::JWTAuth module:
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
end
5
end
Copied!
We create the current_user accessor as usual, but we won't be able to set it until someone successfully create a subscription to a channel. If they fail to pass a valid token, we can deny them a subscription. That means that all channels will need to be able to authenticate tokens during the subscription creation process. We will create a subscribed method in ApplicationCable, which all of your channels inherit from.
app/channels/application_cable/channel.rb
1
module ApplicationCable
2
class Channel < ActionCable::Channel::Base
3
attr_accessor :current_user
4
5
def subscribed
6
authenticate_user!
7
end
8
9
private
10
11
def authenticate_user!
12
@current_user ||= decode_user params[:token]
13
reject unless @current_user
14
connection.current_user = @current_user
15
end
16
17
def decode_user(token)
18
Warden::JWTAuth::UserDecoder.new.call token, :user, nil if token
19
rescue JWT::DecodeError
20
nil
21
end
22
end
23
end
Copied!
In this configuration, a failure to match a token with a Warden user results in a call to reject. This means that while they have successfully established an ActionCable connection, they do not have the credentials to subscribe to the individual channel. Notice how we manually set the current_user on the connection if the authentication is successful.
In order for this scheme to work, all of your ActionCable channels - including StimulusReflex - must conform to the same validation mechanism. StimulusReflex itself will access the ApplicationCable::Channel definition in your application. You can set additional channels to authenticate in this manner by making sure that they inherit from ApplicationCable::Channel and that the subscribed method calls super before your stream_from or stream_for statement:
app/channels/test_channel.rb
1
class TestChannel < ApplicationCable::Channel
2
def subscribed
3
super
4
stream_from "test"
5
end
6
end
Copied!
app/javascript/channels/test_channel.js
1
import consumer from './consumer'
2
3
consumer.subscriptions.create(
4
{
5
channel: 'TestChannel',
6
token: document.querySelector('meta[name=action-cable-auth-token]').content
7
},
8
{
9
connected () { console.log('Token accepted') },
10
rejected () { console.log('Token rejected') }
11
}
12
)
Copied!
Set a JWT token for the current user in your layout template. Note that in this example we do assume that the warden-jwt_auth gem is in your project (possibly through devise-jwt) and that there is a valid current_user accessor in scope.
app/controllers/application_controller.rb
1
class ApplicationController < ActionController::Base
2
before_action do
3
@token = Warden::JWTAuth::UserEncoder.new.call(current_user, :user, nil).first
4
end
5
end
Copied!
app/views/layout/application.html.erb
1
<head>
2
<meta name="action-cable-auth-token" content="<%= @token %>"/>
3
</head>
Copied!
Now, make sure that StimulusReflex is able to access the JWT token from your DOM:
app/javascript/controllers/index.js
1
import { Application } from 'stimulus'
2
import { definitionsFromContext } from 'stimulus/webpack-helpers'
3
import StimulusReflex from 'stimulus_reflex'
4
5
const application = Application.start()
6
const context = require.context('controllers', true, /_controller\.js$/)
7
const params = { token: document.head.querySelector('meta[name=action-cable-auth-token]').content }
8
application.load(definitionsFromContext(context))
9
10
StimulusReflex.initialize(application, { params })
Copied!
Finally, delegate current_user to the ActionCable connection as you would in any other Reflex class:
app/reflexes/example_reflex.rb
1
class ExampleReflex < ApplicationReflex
2
delegate :current_user, to: :connection
3
end
Copied!

Unauthenticated Connections

Perhaps your application doesn't have users. And maybe it doesn't even have sessions! You just want to offer all visitors access for the duration of the time that they are looking at your page. This will give every browser looking at your page a unique ActionCable connection.
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :uuid
4
5
def connect
6
self.uuid = SecureRandom.urlsafe_base64
7
end
8
end
9
end
Copied!
While there is no user concept in this scenario, you can still access the visitor's uuid:
app/reflexes/example_reflex.rb
1
class ExampleReflex < ApplicationReflex
2
delegate :uuid, to: :connection
3
end
Copied!

Hybrid Anonymous + Authenticated Connections

When you are building an application which has authenticated users, but you wish to provide Reflex-powered functionality to all users of your site, you can combine multiple authentication strategies.
Here is an ActionCable connection class based on encrypted session cookies and Devise logins:
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
identified_by :session_id
5
6
def connect
7
self.current_user = env["warden"].user
8
self.session_id = request.session.id
9
reject_unauthorized_connection unless self.current_user || self.session_id
10
end
11
end
12
end
Copied!
This makes use of the ability to declare multiple identified_by values in a single connection class. Note that you still have to delegate both current_user and session_id to the connection so you can access these values in your Reflex action methods.
This approach could make some operations more complicated, because you cannot take for granted that a connection is attached to a valid user. Please ensure that you are double-checking that all destructive mutations are properly guarded based on whatever policies you have in place.

Multi-Tenant Applications

Use of the acts_as_tenant gem has skyrocketed since the excellent JumpStart Pro came out. It's easy to create Reflexes that automatically support tenant scopes.
While a multi-tenant tutorial is out-of-scope for this document, the basic idea of the gem is that you have a model - often Account - that other models get scoped to. If you have an Image class that acts_as_tenant :account then every query (read and write) to the Image class will automatically include a WHERE clause restricting results to the current Account.
As is so typically the case with Rails, the actual technique for bringing the Tenant to your Reflex is shorter than the explanation. Just set the current tenant to an instance of the correct class in your Connection module:
app/channels/application_cable/connection.rb
1
module ApplicationCable
2
class Connection < ActionCable::Connection::Base
3
identified_by :current_user
4
5
def connect
6
self.current_user = env["warden"].user
7
ActsAsTenant.current_tenant = current_user.account
8
end
9
10
end
11
end
Copied!
A slightly more sophisticated reference application with multiple account support and a Current object is available in the tenant branch of the stimulus_reflex_harness repo, if you'd like to dig into this approach further.

Authorization

Just because you are authenticated as a user doesn't mean you should have access to every function in the system. Sometimes you need to enforce roles and privilege levels in your Reflex classes.
The before_reflex callback is the best place to handle privilege checks, because you can call throw :abort to prevent the Reflex if the user is making decisions above their pay grade.

CanCanCan

When using CanCanCan (CCC) for authorization, the accessible_by method ensures that you only access records permitted for the current user. Depending on your requirements, you might opt to use different strategies for Page Morphs than you do for other types of Reflexes. This is because the CCC authorize! method is designed to operate on the current ActionController instance. StimulusReflex only creates Controller instances for Page Morphs, as they incur a performance penalty.
The first solution that you should consider is to create an Ability instance for your user in your Reflex class. This is a technique that the CCC documentation describes as "working in a Pundit way". While it might be a departure from how you use CCC in your Controllers, it does have the advantage of working with all Morph types and doesn't force the instantiation of an otherwise unused Controller instance:
1
class ClassroomsReflex < ApplicationReflex
2
def select_school
3
if element.value.present?
4
abilties = Ability.new(current_user)
5
school = School.find(element.value)
6
classrooms = school.classrooms.accessible_by(abilities)
7
else
8
school = nil
9
classrooms = Classroom.none
10
end
11
# uncomment for a Selector Morph
12
# morph "#classrooms", render(partial: "classrooms/classrooms", locals: { school: school, classrooms: classrooms })
13
end
14
end
Copied!

Page Morphs

Since Page Morphs create an ActionController instance to render your page template, it's possible to piggy-back on your existing Controller-based CCC logic by moving authorization calls out of your Reflex and into your Controller:
1
class ClassroomsReflex < ApplicationReflex
2
def select_school
3
@school = element.value.present? ?
4
School.find(element.value) :
5
nil
6
end
7
end
Copied!
1
class ClassroomsController < ApplicationController
2
def index
3
authorize! :index, School
4
authorize! :index, Classroom
5
@school ||= School.find(params[:school_id)
6
@schools ||= School.accessible_by(current_ability)
7
@classrooms ||= @school.present? ?
8
@school.classrooms.accessible_by(current_ability) :
9
Classroom.none
10
end
11
end
Copied!
While it is possible to create a solution for non-Page Morph Reflexes that involves creating a Controller instance and delegating current_ability to it, it's hard to justify documenting that approach here since there is already a viable, one-size-fits-all solution available and there is a performance hit when you create a Controller.
You cannot use the authorize! method in your Reflex action, because a Reflex is not a Controller.

Pundit

The trusty pundit gem allows you to set up policy classes that you can use to lock down Reflex action methods in a structured way. Reflexes are similar enough to controllers that if you include the Pundit module, you can take advantage of the authorize method.
Pundit expects you to have a current_user in scope and a policy matching the name of your Reflex action. In the following example we create a sing? policy for our sing Reflex action in song_policy.rb
app/policies/song_policy.rb
1
class SongPolicy < ApplicationPolicy
2
def sing?
3
user.sings_in_key?
4
end
5
end
Copied!
app/reflexes/song_reflex.rb
1
class SongReflex < ApplicationReflex
2
include Pundit
3
4
def sing
5
@song = Song.find(params[:song_id])
6
authorize @song
7
# sing your heart out, baby!
8
end
9
end
Copied!
Pundit will match your Reflex action to the right policy. If the authorize call fails, a Pundit::NotAuthorizedError will be raised, which you can handle in your Reflex action or leave unhandled so that it bubbles up and gets picked up by a 3rd-party error handling mechanism such as Sentry or HoneyBadger.
app/reflexes/application_reflex.rb
1
class ApplicationReflex < StimulusReflex::Reflex
2
rescue_from Pundit::NotAuthorizedError do |exception|
3
# handle authorization issue
4
end
5
end
Copied!
If you're using Pundit to safeguard data from being accessed by bad actors and unauthorized parties - due to bugs in your code - that's probably the correct approach. However... you might also want to explicitly validate policies so that you can react to them in your browser:

Explicit policy validation

You can also ask Pundit to validate a policy explicitly and then abort the Reflex before it begins. This is an action that can be handled by the client via the halted life-cycle event.
The following example assumes that you have a current_user in scope and an application_policy.rb already in place. In this application, the User model has a boolean attribute called admin.
app/policies/example_reflex_policy.rb
1
class ExampleReflexPolicy < ApplicationPolicy
2
def test?
3
user.admin?
4
end
5
end
Copied!
app/reflexes/example_reflex.rb
1
class ExampleReflex < ApplicationReflex
2
delegate :current_user, to: :connection
3
4
before_reflex do
5
unless ExampleReflexPolicy.new(current_user, self).test?
6
puts "DENIED"
7
throw :abort
8
end
9
end
10
11
def test
12
puts "We are authorized!"
13
end
14
end
Copied!
You can even pick up this failure to thrive in a callback on your Stimulus controller:
app/javascript/controllers/example_controller.js
1
import ApplicationController from './application_controller'
2
3
export default class extends ApplicationController {
4
connect () {
5
super.connect()
6
}
7
8
testHalted () {
9
console.log('DENIED!')
10
}
11
}
Copied!

Passing params to ActionCable

It's common to pass key/value pairs to your ActionCable subscriptions, which show up as a params hash in your ActionCable Channel class. While it's usually not necessary to send extra information to the StimulusReflex Channel, it is a mechanism available to you. You might have used it to implement the token-based JWT auth technique above.
In this example, we want to tell the server whether the user has granted permission to send them native notifications. We'll then pick it up on the server:
app/javascript/controllers/index.js
1
import { Application } from 'stimulus'
2
import { definitionsFromContext } from 'stimulus/webpack-helpers'
3
import StimulusReflex from 'stimulus_reflex'
4
import consumer from '../channels/consumer'
5
import controller from './application_controller'
6
7
const application = Application.start()
8
const context = require.context('controllers', true, /_controller\.js$/)
9
10
let params
11
Notification.requestPermission().then(notifications => {
12
params = { notifications }
13
}
14
15
application.load(definitionsFromContext(context))
16
StimulusReflex.initialize(application, { consumer, controller, params })
Copied!
app/channels/application_cable/channel.rb
1
module ApplicationCable
2
class Channel < ActionCable::Channel::Base
3
attr_accessor :notifications
4
5
def subscribed
6
@notifications = params[:notifications]
7
puts @notifications # "default", "granted" or "denied"
8
end
9
10
end
11
end
Copied!
Once you know if you can send notifications, you could consider using CableReady's notification operation to send updates. If they denied your request, you could use the Rails flash object instead.
Last modified 2d ago