19

Why am I not able to retrieve current_user inside my channel or how should I retrieve current_user?

What do I use?

  • Rails 5.0.1 --api (I do NOT have any views NOR use coffee)
  • I use react-native app to test this (Works fine WITHOUT authorization)
  • I do NOT use devise for auth (I use JWT instead using Knock, so no cookies)

Trying to get current_user inside my ActionCable channel as described in rubydoc.info

The code looks like

class MessageChannel < ApplicationCable::Channel
  identified_by :current_user

  def subscribed
    stream_from 'message_' + find_current_user_privileges
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  protected

  def find_current_user_privileges
    if current_user.has_role? :admin
      'admin'
    else
      'user_' + current_user.id
    end
  end

end

And running it, I get this error:

[NoMethodError - undefined method `identified_by' for MessageChannel:Class]

And if I remove identified_by :current_user, I get

[NameError - undefined local variable or method `current_user' for #<MessageChannel:0x7ace398>]
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Saravanabalagi Ramachandran
  • 8,551
  • 11
  • 53
  • 102

5 Answers5

18

If you see the doc you provided, you will know that identified_by is not a method for a Channel instance. It is a method for Actioncable::Connection. From Rails guide for Actioncable Overview, this is how a Connection class looks like:

#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 current_user = User.find_by(id: cookies.signed[:user_id])
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

As you can see, current_user is not available here. Instead, you have to create a current_user here in connection.

The websocket server doesn't have a session, but it can read the same cookies as the main app. So I guess, you need to save cookie after authentication.

Tom Lord
  • 27,404
  • 4
  • 50
  • 77
Sajan
  • 1,893
  • 2
  • 19
  • 39
  • I have not explored it much but for now I can think of only one other way i.e sending a query parameter like when using the default cable.js provided by rails , use `App.cable = ActionCable.createConsumer('/cable?token=12345');` and the use `request.params[:token]` in connection class. But beware that, I don't know if exposing token like this is secure or not. For me a signed cookie is the best way. You can set it easily after login. – Sajan Feb 25 '17 at 06:11
  • yeah, it is insecure to expose the token that way :/ But it is so much like a cookie, just like how if someone get the cookie they can steal the session, the same way, if someone gets the jwt string, they can authenticate with the server till it expires, just like the cookie – Saravanabalagi Ramachandran Feb 25 '17 at 06:20
  • 1
    where is `cookies` _in `cookies.signed[:user_id]`_ obj pulled from? Request headers? If yes, I can pull jwt the same way too..! requestHeader['Authorization'] would yield it... – Saravanabalagi Ramachandran Feb 25 '17 at 06:31
  • I agree but stealing cookie is a little more harder than the exposed token. And here I was implying to to set a cookie with only user_id, make it a signed cookie so that it will be encrypted. And on logout clear the cookie. – Sajan Feb 25 '17 at 06:32
  • No custom header. Every requests send the cookie along with other data. Browser sends the cookie by default so I guess, you don't have to set that manually. So that's just rails way to get the `cookie`. – Sajan Feb 25 '17 at 06:33
  • Thanks @Sajan, for kickstarting me in the right path... I have created a repo [action-cable-react-jwt](https://github.com/zekedran/action-cable-react-jwt) to further help getting current_user using JWT, esp when you are using: **Rails, React-Native and JWT**. Since websocket request mysteriously didn't allow custom headers to be sent, I just did a dirty hack to authenticate JWTs _(by appending JWT in an existing header :D )_ – Saravanabalagi Ramachandran Aug 04 '18 at 12:56
14

if you are using devise gems in rails, Please replace this function:

def find_verified_user # this checks whether a user is authenticated with devise
  if verified_user = env['warden'].user
    verified_user
  else
    reject_unauthorized_connection
  end
end

I hope this will help you.

moveson
  • 5,103
  • 1
  • 15
  • 32
Sochetra Nov
  • 335
  • 2
  • 7
4

After setting self.current_user in ApplicationCable::Connection it become available in the channel instances. So you can set up your authentication like Sajan wrote and just use current_user in MessageChannel

For example this code worked for me

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

    def connect
      self.verified_user = find_verified_user
    end

    private

    def current_user
      jwt = cookies.signed[:jwt]
      return unless jwt

      decoded = JwtDecodingService.new(jwt).decrypt!
      @current_user ||= User.find(decoded['sub']['user_id'])
    end

    def find_verified_user
      current_user || reject_unauthorized_connection
    end
  end
end

class NextFeaturedPlaylistChannel < ApplicationCable::Channel
  def subscribed
    stream_from "next_featured_playlist_#{verified_user.id}"
  end
end

Axife
  • 99
  • 1
  • 4
2

Well, in theory:

  • You have access to the cookies in the ActiveCable::Connection class.
  • You can set and receive cookies.signed and cookies.encrypted
  • Both the application and ActionCable share same configuration, therefore they share same `secret_key_base'

So, if you know the name of your session cookie (somehow obvious, let it be called "_session"), you can simply receive the data in it by:

cookies.encrypted['_session']

So, you should be able to do something like:

user_id = cookies.encrypted['_session']['user_id']

This depends on do you use cookie store for the session and on the exact authentication approach, but in any case the data you need should be there.

I found this approach more convenient as the session is already managed by the authentication solution you use and you more likely don't need to care about things like cookie expiration and duplication of the authentication logic.

Here is more complete example:

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

    def connect
      session = cookies.encrypted['_session']
      user_id = session['user_id'] if session.present?

      self.current_user = (user_id.present? && User.find_by(id: user_id))

      reject_unauthorized_connection unless current_user
    end
  end
end

Not Entered
  • 193
  • 1
  • 8
1

For Rails 5 API mode:

application_controller.rb

class ApplicationController < ActionController::API
   include ActionController::Cookies
   ...
   token = request.headers["Authorization"].to_s
   user = User.find_by(authentication_token: token)
   cookies.signed[:user_id] = user.try(:id)

connection.rb

class Connection < ActionCable::Connection::Base
   include ActionController::Cookies
   ...
   if cookies.signed[:user_id] && current_user = User.where(id: cookies.signed[:user_id]).last
     current_user
   else
     reject_unauthorized_connection
   end

config/application.rb

config.middleware.use ActionDispatch::Cookies
Abel
  • 3,989
  • 32
  • 31