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:

app/channels/application_cable/channel.rb
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
end
end

The connection.current_user is set during the WebSocket handshake in ApplicationCable::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
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
end
end

Authorizing Channel Actions

Use authorize! in channel action methods to guard access:

app/channels/chat_channel.rb
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
)
end
end

When 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
end
end

Authorizing 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
end
end

If 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
end
end

Verifying 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)
end
end

Cover 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:

config/initializers/action_policy.rb
Rails.application.config.action_policy.auto_inject_into_channel = false

Reusing 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.

Powered by WebContainers