Send auth_token for authentication to ActionCable
Asked Answered
Q

10

23
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      #puts params[:auth_token]
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.name
   end

  end
end

I don't use web as end point for action cable, so I want to use auth_token for authentication. By default action cable use session user id for authentication. How to pass params to connect method?

Quits answered 19/2, 2016 at 9:37 Comment(1)
The official docs have an example with a token in the URL here: guides.rubyonrails.org/…Confutation
L
54

I managed to send my authentication token as a query parameter.

When creating my consumer in my javascript app, I'm passing the token in the cable server URL like this:

wss://myapp.com/cable?token=1234

In my cable connection, I can get this token by accessing the request.params:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.name
    end

    protected:
    def find_verified_user
      if current_user = User.find_by(token: request.params[:token])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

It's clearly not ideal, but I don't think you can send custom headers when creating the websocket.

Logging answered 10/4, 2016 at 19:2 Comment(11)
I was practically typing this in when I found your answer. Thanks!Goniometer
Did anyone else get this to work? Mine is still not connecting with this. When you're passing in the token in the 'cable server URL', this is the part in development.rb or production.rb, right?Cowslip
Update: Heads up to anyone else: You'll put the 'wss://myapp.com/cable?token=1234' in your cable.js file.Cowslip
I wonder is this approach secure. Thing that concerns me is that nobody ever does this with usual HTTP request, but why then, if it is fine?Commitment
anyone have a comment on the security of this method?Terrorize
Typically server logs contain references to server requests. This could be an issue considering this approach could have auth tokens spread throughout log files.Manhunt
@Manhunt But you can filter it on the application.rb file.Inquest
It should be okay as long as you are using HTTPS to send your requests in production. Just like any other login form submission. As Osmond mentioned you'll want to filter the log. Probably not the biggest problem if using one time tokens. @JoeHalfFaceBasement
Related answer: #32723452Selachian
Although this is about OAuth you can find a good summary of why you should not take this approach here: tools.ietf.org/html/rfc6750#section-5.3. I will add my own answer on how I implemented authentication.Serenaserenade
Passing token in the url is very insecure #32723452Wales
G
17

Pierre's answer works. However, it's a good idea to be explicit about expecting these parameters in your application.

For instance, in one of your config files (e.g. application.rb, development.rb, etc...) you can do this:

config.action_cable.mount_path = '/cable/:token'

And then simply access it from your Connection class with:

request.params[:token]
Gunplay answered 8/8, 2016 at 11:14 Comment(1)
shouldn't that be /cable?:token instead of /cable/:token? The token is coming as a query param I suppose and not as an actual routeAmathist
M
10

Unfortunately for websocket connections, additional headers and custom ones are not supported1 by most2 websocket clients and servers. So the possible options are:

  • Attach as an URL parameter and parse it on the server

    path.to.api/cable?token=1234
    
    # and parse it like
    request.params[:token]
    

Cons: It could be vulnerable as it may end up in logs and system process information available to others that have access to the server, more here

Solution: Encrypt the token and attach it, so even if it can be seen in the logs, it would serve no purpose until its decrypted.

  • Attach JWT in one of the allowed parameters.

Client side:

# Append jwt to protocols
new WebSocket(url, existing_protocols.concat(jwt))

I created a JS library action-cable-react-jwt for React and React-Nativethat just does this. Feel free to use it.

Server side:

# get the user by 
# self.current_user = find_verified_user

def find_verified_user
  begin
    header_array = self.request.headers[:HTTP_SEC_WEBSOCKET_PROTOCOL].split(',')
    token = header_array[header_array.length-1]
    decoded_token = JWT.decode token, Rails.application.secrets.secret_key_base, true, { :algorithm => 'HS256' }
    if (current_user = User.find((decoded_token[0])['sub']))
      current_user
    else
      reject_unauthorized_connection
    end
  rescue
    reject_unauthorized_connection
  end
end

1 Most Websocket APIs (including Mozilla's) are just like the one below:

The WebSocket constructor accepts one required and one optional parameter:

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString protocols
);

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString[] protocols
);

url

The URL to which to connect; this should be the URL to which the WebSocket server will respond.

protocols Optional

Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to be able to handle different types of interactions depending on the specified protocol). If you don't specify a protocol string, an empty string is assumed.

2 There are always excpetions, for instance, this node.js lib ws allows building custom headers, so you can use the usual Authorization: Bearer token header, and parse it on the server but both client and server should use ws.

Mcfadden answered 22/3, 2017 at 9:1 Comment(0)
S
6

As I already stated in a comment the accepted answer is not a good idea, simply because the convention is that the URL should not contain such sensitive data. You can find more information here: https://www.rfc-editor.org/rfc/rfc6750#section-5.3 (though this is specifically about OAuth).

There is however another approach: Use HTTP basic auth via the ws url. I found that most websocket clients allow you to implicitly set the headers by prepending the url with http basic auth like this: wss://user:[email protected]/cable.

This will add the Authorization header with a value of Basic .... In my case I was using devise with devise-jwt and simply implemented a strategy which inherited from the one provided in the gem which pulls the jwt out of the Authorization header. So I set the url like this: wss://[email protected]/cable which sets the header to this (pseudo): Basic base64("token:") and parse that in the strategy.

Serenaserenade answered 25/10, 2018 at 10:49 Comment(4)
Good solution and absolutely correct: sensitive information should not be part of the URL. Any chance you'd mind sharing a link to that strategy's gist? Curious to see what your implementation looks like. I'm also using devise-jwt and trying to solve this problem.Wino
In the end I found this too hacky and switched to another solution. I built a dedicated action for authorization. It sets the current_user. The other actions then check if this is set and otherwise reject. So you had to get into the channel first, the call the auth action, then call whatever authenticated action. I encapsulated this into my ApplicationChannel. Does it make sense, @subvertallchris?Serenaserenade
Ah, I forgot I even made another answer with this. https://mcmap.net/q/554162/-send-auth_token-for-authentication-to-actioncableSerenaserenade
Yeah, that makes perfect sense and I completely agree. I explored some basic auth approaches and none of them felt right, so I settled on what you're describing, too. Thanks for following up!Wino
S
2

I was asked about it recently and want to share the solution that I currently use in production systems.

class MyChannel < ApplicationCable::Channel
  attr_accessor :current_user

  def subscribed
    authenticate_user!
  end

  private

  # this works, because it is actually sends via the ws(s) and not via the url <3
  def authenticate_user!
    @current_user ||= JWTHelper.new.decode_user params[:token]

    reject unless @current_user
  end
end

Then re-use warden strategies to work with that JWT (and let it handle all possible edge cases and pitfalls).

class JWTHelper
  def decode_user(token)
    Warden::JWTAuth::UserDecoder.new.call token, :user, nil if token
  rescue JWT::DecodeError
    nil
  end

  def encode_user(user)
    Warden::JWTAuth::UserEncoder.new.call(user, :user, nil).first
  end
end

Though I didn't use ActionCable for the frontend it should roughly work like this:

this.cable.subscriptions.create({
  channel: "MyChannel",
  token: "YOUR TOKEN HERE",
}, //...
Serenaserenade answered 16/6, 2020 at 20:24 Comment(0)
W
1

In case any of you would like to use ActionCable.createCustomer. But have renewable token as I do:

const consumer = ActionCable.createConsumer("/cable")
const consumer_url = consumer.url
Object.defineProperty(
  consumer, 
  'url', 
  {
      get: function() { 
        const token = localStorage.getItem('auth-token')
        const email = localStorage.getItem('auth-email')
        return consumer_url+"?email="+email+"&token="+token
      }
  });
return consumer; 

Then in case that the connection is lost it will be opened with a fresh new token.

Wary answered 2/2, 2018 at 8:57 Comment(0)
L
1

to add to previous answers, if you used your JWT as a param, you're going to have to at least btoa(your_token) @js and Base64.decode64(request.params[:token]) @rails as rails considers dot '.' a separator so your token will be cut off @rails params side

Lemcke answered 19/4, 2018 at 18:36 Comment(0)
S
1

Another way (the way I did it in the end instead of my other answer) would be to have a authenticate action on your channel. I used this to determine the current user and set it in the connection/channel. All the stuff is send over websockets so credentials are not an issue here when we have it encrypted (i.e. wss).

Serenaserenade answered 26/10, 2018 at 11:41 Comment(0)
S
0

As for security of Pierre's answer: If you're using WSS protocol, which uses SSL for encryption, then the principles for sending secure data should the same as for HTTPS. When using SSL, query string parameters are encrypted as well as the body of the request. So if in HTTP APIs you're sending any kind of token through HTTPS and deem it secure, then it should be the same for WSS. Just remember that the same as for HTTPS, don't send credentials like password through query parameters, as the URL of the request could be logged on a server and thus stored with your password. Instead use things like tokens that are issued by the server.

Also you can check this out (this basically describes something like JWT authentication + IP address verification): https://devcenter.heroku.com/articles/websocket-security#authentication-authorization.

Strain answered 14/11, 2017 at 18:5 Comment(0)
D
-1

It is also possible to pass the authentication token in the request headers and then validate the connection by accessing the request.headers hash. For example, if the authentication token were specified in a header called 'X-Auth-Token' and your User model have a field auth_token you could do:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.id
    end

    protected

    def find_verified_user
      if current_user = User.find_by(auth_token: request.headers['X-Auth-Token'])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
Dhiren answered 25/6, 2016 at 22:5 Comment(5)
Sadly, headers can't be set for WebSocket connections. So this answer is actually misleading and invalid :/Avram
@Avram if that is the case, than how would one use actioncable with iOS?Cowslip
@Cowslip You'd need to pursue the query hash approach outlined above I believeAvram
@acorncom, sorry for necroposting, but i don't think you are absolutely right. The thing is that handshake is proceeded through usual HTTP request. Using faye-websocket gem, which handles websocket connection in rack, I managed to get header, authorize user and then open/close connection and it is fully working. Not sure if it possible using action cableCommitment
Hi Joe, it's actually possible as per the solution I proposed there before. For pure websocket connections headers can't be set - in principle, but in ActionCable you actually can. And they can be extracted, at Cable Connection side.Dhiren

© 2022 - 2024 — McMap. All rights reserved.