# WebSocket Subscription Rejection Fix ## Issue **Error**: "GatewayChannel is transmitting the subscription rejection" ## Root Cause The `GatewayChannel#subscribed` method was attempting to re-authenticate the gateway using `params[:api_key_digest]`, but this caused two problems: 1. **Double Authentication**: The gateway was already authenticated at the connection level in `ApplicationCable::Connection#connect` 2. **Missing Parameter**: The channel was expecting `api_key_digest` as a channel parameter, but it was never being passed 3. **Wrong Layer**: Authentication should happen at the connection level (before subscription), not at the channel subscription level ## Authentication Flow ### Correct Flow (After Fix) ``` 1. Client connects to WebSocket URL: ws://host/cable?api_key=gw_live_... 2. ApplicationCable::Connection#connect - Extracts api_key from query params - Hashes it with SHA256 - Finds gateway by api_key_digest - Sets current_gateway - Connection accepted ✅ 3. Client subscribes to GatewayChannel Message: {"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"} 4. GatewayChannel#subscribed - Uses current_gateway (already authenticated) - Streams to gateway-specific channel - Updates heartbeat - Subscription accepted ✅ ``` ### Previous Flow (Before Fix) ``` 1. Client connects to WebSocket URL: ws://host/cable?api_key=gw_live_... 2. ApplicationCable::Connection#connect - Authenticates gateway - Connection accepted ✅ 3. Client subscribes to GatewayChannel Message: {"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"} 4. GatewayChannel#subscribed (BROKEN) - Looks for params[:api_key_digest] ❌ (doesn't exist) - Gateway lookup fails ❌ - Subscription rejected ❌ ``` ## The Fix ### Before (Broken) ```ruby class GatewayChannel < ApplicationCable::Channel def subscribed # Authenticate gateway using API key from params api_key_digest = params[:api_key_digest] # ❌ This was never passed! unless api_key_digest reject # ❌ Always rejected here return end @gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true) unless @gateway reject return end stream_for @gateway @gateway.heartbeat! Rails.logger.info("Gateway #{@gateway.device_id} connected via WebSocket") end end ``` ### After (Fixed) ```ruby class GatewayChannel < ApplicationCable::Channel def subscribed # Gateway is already authenticated at the connection level # current_gateway is set by ApplicationCable::Connection @gateway = current_gateway # ✅ Use already-authenticated gateway unless @gateway reject return end # Subscribe to gateway-specific channel stream_for @gateway # Update gateway status @gateway.heartbeat! Rails.logger.info("Gateway #{@gateway.device_id} subscribed to GatewayChannel") end end ``` ## Key Changes 1. **Removed** `params[:api_key_digest]` lookup (it was never passed) 2. **Use** `current_gateway` which is set by the connection authentication 3. **Simplified** authentication - it only happens once at the connection level 4. **Removed** redundant authentication check ## Connection Authentication The connection-level authentication in `ApplicationCable::Connection` is the correct place for this: ```ruby module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_gateway def connect self.current_gateway = find_verified_gateway logger.info "Gateway #{current_gateway.device_id} connected" end private def find_verified_gateway # Get API key from request params (query string) api_key = request.params[:api_key] if api_key.blank? reject_unauthorized_connection end # Hash the API key and find gateway api_key_digest = Digest::SHA256.hexdigest(api_key) gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true) if gateway gateway else reject_unauthorized_connection end end end end ``` ## Testing the Fix ### Test with wscat ```bash wscat -c "ws://localhost:3000/cable?api_key=gw_live_your_key_here" ``` **After connection**, subscribe to the channel: ```json {"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"} ``` **Expected Response**: ```json { "identifier": "{\"channel\":\"GatewayChannel\"}", "type": "confirm_subscription" } ``` ### Test from Android ```kotlin val wsUrl = "ws://192.168.1.100:3000/cable?api_key=$apiKey" val request = Request.Builder().url(wsUrl).build() webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { // Connection successful // Subscribe to channel val subscribeMsg = JSONObject().apply { put("command", "subscribe") put("identifier", """{"channel":"GatewayChannel"}""") } webSocket.send(subscribeMsg.toString()) } override fun onMessage(webSocket: WebSocket, text: String) { val json = JSONObject(text) if (json.optString("type") == "confirm_subscription") { // Subscription successful! ✅ Log.d(TAG, "Subscribed to GatewayChannel") } } }) ``` ## Why This Matters ### Before Fix - ❌ All subscription attempts were rejected - ❌ Gateways couldn't receive SMS send commands - ❌ Real-time communication was broken - ❌ Messages stuck in "pending" state ### After Fix - ✅ Subscriptions work correctly - ✅ Gateways receive SMS send commands in real-time - ✅ Bidirectional communication enabled - ✅ Messages sent immediately to online gateways ## Related Files - `app/channels/application_cable/connection.rb` - Connection authentication (unchanged, already correct) - `app/channels/gateway_channel.rb` - Channel subscription (FIXED) - `CABLE_DOCUMENTATION.md` - WebSocket integration guide (already correct) - `API_DOCUMENTATION.md` - Full API docs (already correct) ## Summary The fix removes redundant authentication from the channel subscription layer and properly uses the `current_gateway` that was already authenticated at the connection level. This follows Action Cable best practices where: 1. **Connection authentication** happens when WebSocket connects 2. **Channel subscription** happens after connection is authenticated 3. **No re-authentication** needed in channels - use `current_gateway` or other identifiers set at connection level WebSocket connections from Android devices should now work correctly! 🚀