I am setting up a chat feature using ActionCable in a Rails API / React project, and trying to figure out the best solution to authenticate connections.
The project uses the devise-jwt gem, so my Authorization is passed as a header. Since Web Sockets does not allow headers, I've been searching for the best alternative. Solutions I have found suggest one of the following (with various caveats):
Neither of these solutions feel particular nice, but I've tried to go with the second because sending sensitive information in the url (even an encoded JWT) just feels wrong.
Here is the code in my 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
begin
jwt = request.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
decoded_jwt = JWT.decode(jwt, Rails.application.credentials.devise_jwt_secret_key)[0]
id = decoded_jwt['sub']
case decoded_jwt['scp']
when 'admin'
Admin.where(id: id).first
when 'hero'
Hero.where(id: id).first
when'villain'
Villain.where(id: id).first
else
reject_unauthorized_connection
end
rescue
reject_unauthorized_connection
end
end
end
end
I have three different User models: Admin, (and let's call them) Hero & Villain, because reasons.
Now, my actual probelm: In the server I can get the JWT, decode it, and find my Admin, but in the chrome console when I try to create the socket I get: WebSocket connection to 'ws://localhost:3000/cable' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received
That is in response to running the following code in the chrome console:
token = "jwt.etc.blah..."
socket = new WebSocket(url, [token]);
The server itself seems alright with things:
Started GET "/cable" for ::1 at 2020-07-22 18:26:02 +0100
Started GET "/cable/" [WebSocket] for ::1 at 2020-07-22 18:26:02 +0100
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
Finished "/cable/" [WebSocket] for ::1 at 2020-07-22 18:26:02 +0100
Admin Load (0.2ms) SELECT "admins".* FROM "admins" WHERE "admins"."id" = $1 ORDER BY "admins"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/channels/application_cable/connection.rb:19:in `find_verified_user'
Registered connection (Z2lkOi8vYXBpNTUtbWFpcy9BZG1pbi8x)
The error seems to be telling that I need some kind of confirmation or information about the extra protocol that I've hackily snuck my JWT into... but I've no idea how to go about doing this.
Any help would be greatly appreciated! Is it even possible to send additional protocols with rails ActionCable? Or am I going to have to resort to using a query param? Or is there an even neater way that I can tackle this problem?
First time I've tried anything with web sockets, so thanks in advance for help!
Passing credentials via Sec-WebSocket-Protocol is a common workaround for WebSocket authentication for web based applications since browsers do not support sending custom headers while connection initiation.
If you are passing a value via Sec-WebSocket-Protocol header, your server has to return the same header/value pair back to the client.
Since Sec-WebSocket-Protocol header is not intended for passing credentials to the WebSocket server, you have to play by its rules and treat it like a protocol and fulfill its requirements.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With