ActionCable Authorization
Real-time features built with Action Cable need authorization just like your HTTP endpoints. A user subscribing to a channel or performing a channel action should only be allowed to do so if they have permission.
How Action Policy Integrates
Action Policy automatically integrates with Action Cable when you include it in your application. The ActionPolicy::Behaviour module is injected into ActionCable::Channel::Base, so your channels have the same authorize! and allowed_to? methods you use in controllers.
Setting Up the Authorization Context
Action Cable channels run in a different context than HTTP controllers. You need to tell Action Policy where to find the current user. The standard place is ApplicationCable::Channel:
module ApplicationCable class Channel < ActionCable::Channel::Base # Make the connection's current_user available as the authorization subject authorize :user
private
def user connection.current_user end endendThe connection.current_user is set during the WebSocket handshake in ApplicationCable::Connection:
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.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end endendAuthorizing Channel Actions
Use authorize! in channel action methods to guard access:
class ChatChannel < ApplicationCable::Channel def subscribed chat = Chat.find(params[:chat_id]) authorize! chat, to: :show? stream_for chat end
def follow(data) chat = Chat.find(data["chat_id"]) authorize! chat, to: :show? stream_from chat end
def send_message(data) chat = Chat.find(data["chat_id"]) authorize! chat, to: :update?
chat.messages.create!( body: data["message"], user: user ) endendWhen authorize! raises ActionPolicy::Unauthorized, the channel action stops and the exception propagates. You can rescue it to send a meaningful response to the client:
class ChatChannel < ApplicationCable::Channel rescue_from ActionPolicy::Unauthorized do |exception| # Reject the subscription or transmit an error transmit({ error: "Not authorized: #{exception.message}" }) # Or: reject end
def subscribed chat = Chat.find(params[:chat_id]) authorize! chat, to: :show? stream_for chat endendAuthorizing Subscriptions
To guard the subscription itself, call authorize! inside subscribed before calling stream_for or stream_from:
class NotificationsChannel < ApplicationCable::Channel def subscribed # Reject unauthorized connections at the subscription level authorize! user, to: :subscribe_to_notifications? stream_for user endendIf you call reject inside subscribed, Action Cable will close the connection. Raising ActionPolicy::Unauthorized has the same effect if you add a rescue_from handler that calls reject.
Checking Permissions Without Raising
Use allowed_to? when you want to conditionally stream different data based on permissions:
class ProductChannel < ApplicationCable::Channel def subscribed product = Product.find(params[:product_id])
if allowed_to?(:show?, product) stream_for product else reject end endendVerifying Authorization Coverage
Unlike controllers, Action Policy does not provide a verify_authorized callback for channels — after_action is a Rails controller concept and does not exist in ActionCable channels. There is no built-in mechanism to enforce that every channel action called authorize!.
The recommended approach is to call authorize! explicitly at the top of each action method. To make the requirement visible, you can define a private helper that raises if called without authorization context, but the simplest and most reliable pattern is disciplined use of authorize! in every action:
class ChatChannel < ApplicationCable::Channel def subscribed chat = Chat.find(params[:chat_id]) authorize! chat, to: :show? # explicit — must be present stream_for chat end
def send_message(data) chat = Chat.find(data["chat_id"]) authorize! chat, to: :update? # explicit — must be present chat.messages.create!(body: data["message"], user: user) endendCover this gap with tests: write a channel spec for every action and assert that an unauthorized user cannot perform it.
Disabling Auto-Injection
If for some reason you don’t want Action Policy behavior in your channels, you can disable it:
Rails.application.config.action_policy.auto_inject_into_channel = falseReusing Your Existing Policies
The key advantage here is that your existing policy classes work without modification. The same ChatPolicy you use in ChatsController applies in ChatChannel. Authorization rules are defined once and enforced everywhere.