Introduction
Not every user should be able to subscribe to every channel. Action Cable provides mechanisms to authorize subscriptions and reject unauthorized access, ensuring that sensitive real-time data reaches only the users who should see it.
Key Concepts
- Subscription Rejection: Calling
rejectin thesubscribedcallback refuses the subscription and notifies the client that access was denied. - Authorization Guard: A check in
subscribedthat verifies the current user has permission to access the requested stream. rejectMethod: Prevents the subscription from being established. The client'srejectedcallback fires instead ofconnected.
Real World Context
Consider a project management app with private boards. When a user tries to subscribe to the BoardChannel for a specific board, you must verify they are a member of that board. Without this check, any authenticated user could subscribe to any board's stream and see private updates. This is the WebSocket equivalent of controller authorization — and it is just as critical.
Deep Dive
Here is a channel with proper authorization:
ruby# app/channels/board_channel.rb class BoardChannel < ApplicationCable::Channel def subscribed @board = Board.find_by(id: params[:board_id]) if @board && current_user.member_of?(@board) stream_for @board else reject end end def unsubscribed # Broadcast that the user left BoardChannel.broadcast_to(@board, { type: "presence", user: current_user.username, status: "offline" }) if @board end end
The reject method stops the subscription from being created. No stream is set up, and no data will be sent. On the client side, handle the rejection:
javascriptconsumer.subscriptions.create( { channel: "BoardChannel", board_id: 42 }, { connected() { console.log("Successfully subscribed to board") }, rejected() { console.log("Subscription rejected — you may not have access") // Show an error message or redirect }, received(data) { // Handle board updates } } )
You can also perform authorization checks in channel actions, not just in subscribed:
rubydef update_card(data) card = @board.cards.find(data["card_id"]) unless current_user.can_edit?(card) transmit(error: "You do not have permission to edit this card") return end card.update!(title: data["title"]) BoardChannel.broadcast_to(@board, { type: "card_updated", card_id: card.id, title: card.title }) end
Common Pitfalls
- Authorizing only in
subscribedbut not in actions — A user who was authorized at subscription time might lose access later (e.g., removed from a team). High-security actions should re-check permissions. - Not calling
rejectand not callingstream_from— If you skip both, the subscription is technically "established" but receives no data. This is confusing to debug. Always explicitly reject or stream. - Leaking error details in rejection — Unlike HTTP responses,
rejectdoes not carry a message. Do not try to pass error details through it. Usetransmitbeforerejectif you need to explain why access was denied.
Best Practices
- Always authorize in
subscribed— Treat it like abefore_actionin a controller. Check that the user has access to the resource identified byparams. - Use
find_byinstead offind—findraises an exception if the record does not exist, which would crash the subscription handler.find_byreturnsnil, letting you reject gracefully. - Log rejected subscriptions — Monitoring rejected subscription attempts helps detect abuse or misconfigured clients.
Summary
- Call
rejectinsubscribedto refuse unauthorized subscriptions — the client'srejectedcallback will fire. - Always verify that
current_userhas permission to access the resource identified inparams. - Use
find_byinstead offindto avoid exceptions when resources do not exist. - Re-check authorization in individual channel actions for high-security operations.
- The
rejectmethod does not carry error messages — usetransmitbeforerejectif you need to explain the denial.
Code Examples
class BoardChannel < ApplicationCable::Channel
def subscribed
@board = Board.find_by(id: params[:board_id])
if @board && current_user.member_of?(@board)
stream_for @board
else
reject
end
end
end