ActionCable does not run connect method upon connecting - ruby-on-rails

I'm trying to work with ActionCable, but right now I'm trying to do the seemingly trivial task of trying to get a user id in a channel when I have the connection identified by current_user and its set properly in the connect method. I feel like I'm missing something completely obvious here. Here are my two relevant files
application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = env['warden'].user
end
end
end
notifications_channel.rb
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from("push_notification_channel_#{connection.current_user.id}")
end
end
For some reason connection.current_user is nil in notifications channel. Am I just missing something really obvious?

Related

Using ActionCable with multiple identification methods

I develop a Ruby on Rails 5.1 application using ActionCable. User authentification via Devise works fine for several channels. Now, I want to add a second type of channels which does not require any user authentification. More precisely, I would like to enable anonymous website visitors to chat with support staff.
My current implementation of ApplicationCable::Connection for authenticated users looks like this:
# 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
protected
def find_verified_user
user = User.find_by(id: cookies.signed['user.id'])
return user if user
fail 'User needs to be authenticated.'
end
end
end
Anonymous users will be identified by some random UUID (SecureRandom.urlsafe_base64).
Question:
How do I best add this new type of channels? Could I add a boolean flag require_authentification somewhere, override it in my inherited channel class for anonymous communication, and switch the identification method in Connection depending on this attribute? Or would I rather have to implement a completely new module, say AnonymousApplicationCable?
Hi I came into the same problem, after looking at your solution in rails github comment, I assume it is better to create the token and keep the logic in the connect method.
So what I do was just utillize the the warden checking and if it is nil just create the anonymous token and otherwise. For this to work, I need to declare 2 identifier :uuid and :current_user
class Connection < ActionCable::Connection::Base
identified_by :current_user, :uuid
def connect
if !env['warden'].user
self.uuid = SecureRandom.urlsafe_base64
else
self.current_user = find_verified_user
end
end
protected
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
end

Getting AuthLogic to work with ActionCable

I'm working on a new Rails 5 (RC1) app. I used AuthLogic for user authentication, and it works great as always, until I got to ActionCable.
#app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = UserSession.find
end
end
end
I get the error: You must activate the Authlogic::Session::Base.controller with a controller object before creating objects
I tried:
Authlogic::Session::Base.controller = Authlogic::ControllerAdapters::RailsAdapter.new(self)
But that does not work because the Connection class is not a Controller.
I look at the AuthLogic code, but I can't figure out how to bypass its dependence on a controller object. I just need to load the user's session. Any thoughts?
I figured it out on my own. I feel it is sort of hacky, basically in my ApplicationController I set a secure cookie with the AuthLogic persistence_token, then I can read this token and manually load the user in ActionCable.
class ApplicationController < ActionController::Base
before_action :set_verify_cookie
def set_verify_cookie
#action cable needs a way outside of controller logic to lookup a user
return unless current_user
cookies.signed[:vvc] = current_user.persistence_token
end
end
#app/channels/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', self.current_user.username unless self.current_user.nil?
end
protected
def find_verified_user_or_guest
User.find_by(:persistence_token => cookies.signed[:vvc])
end
end
One potential gotch, the cookie needs to be cleared on logout or ActionCable will still find the user on subsequent page loads.
#app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def destroy
cookies.signed[:vvc] = nil
current_user_session.destroy
flash[:success] = "Logout successful!"
redirect_to root_url
end
end
Assuming you're using Authlogic default, the persistence token is stored in the cookie under the key 'user_credentials'.
So you can lookup your user like this:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
def connect
verify_user
end
private
def verify_user
reject_unauthorized_connection unless verified_user?
end
def verified_user?
cookie_key && User.find_by_persistence_token(token)
end
def token
cookie && cookie.include?('::') && cookie.split("::")[0]
end
def cookie
cookies['user_credentials']
end
end
end

Rails5 ActionCable include current_user method from SessionsHelper

I want to get access to my current_user method which is defined in the SessionsHelper. How can I include or require it for the ApplicationCable Connection class?
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
if current_user
self.current_user = current_user
logger.add_tags current_user.name
end
end
end
end
gives me
There was an exception - NoMethodError(undefined method `name' for nil:NilClass)
Equivalent to the ApplicationController I included the SessionHelper in channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
include SessionsHelper
identified_by :current_user
# (..)
end
end
but doesn't work.
As you can see in the notesof the official documentation the cable can not access the session. In this article, cited in the notes, you can find a way to use cookies for managing athentication

Only connect users to ActionCable

I'm using ActinCable on my application, and I have an issue with authorization. Currently actioncable tries to authorize every single person live on the site, repeatedly as-well.
This returns a constant stream of An unauthorized connection attempt was rejectedin my log. Now that's because people visiting that aren't signed in, are also attempted to gain access.
My connection.rb looks like this:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
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
now I'm wondering if I can make it so that only people that are signed in, try to become authorized by connnection.rbinstead of every visitor using the site. I am too unfamiliar with ActionCable to know how to limit this - and the documentation for ActionCable are still in their early days.
The connection attempt is when ActionCable.createConsumer() is called. You should try to call it only when user is logged in.

Authenticating ActionCable connections

I've found a wonderful ActionCable gem, which is a good solution for SPA.
I want to send only the html, css and js assets, all other connections will be implemented through ActionCable. It's not difficult to exchange strings or integers, but how can I login through ActionCable?
From the Readme
# 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
protected
def find_verified_user
if current_user = User.find(cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end
So it looks like you could insert your own find_verified_user logic here. The reject_unauthorized_connection method lives in lib/action_cable/connection/authorization.rb for reference.
From Heroku:
[authentication] can be done in a variety of ways, as WebSockets will
pass through standard HTTP headers commonly used for authentication.
This means you could use the same authentication mechanism you’re
using for your web views on WebSocket connections as well.
Since you cannot customize WebSocket headers from JavaScript, you’re limited to
the “implicit” auth (i.e. Basic or cookies) that’s sent from the
browser. Further, it’s common to have the server that handles
WebSockets be completely separate from the one handling “normal” HTTP
requests. This can make shared authorization headers difficult or
impossible.
With this in mind it would likely be a real pain not to just use a normal web login flow to set your auth cookie, delivering your SPA after the authentication step, but hopefully this can give you some pointers.
FYI, if you have devise already installed in your application, then you can use the environment variable set by warden to find the authenticated user. For every authenticated user warden stores the user object in environment var. Every request is authenticated by warden middleware.
Note: this env is different from ENV.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user_from_env
end
private
def find_verified_user_from_env
# extracting `user` from environment var
current_user = env['warden'].user
if current_user
current_user
else
reject_unauthorized_connection
end
end
end
end
If you have not used devise then, here is another solution. Precondition is, you will have to set a signed cookie called user_id in your sessions_controller or something like that.
eg
cookies.signed[:user_id] = current_user.id
and for connection:
# 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_from_cookies
end
private
def find_verified_user_from_cookies
current_user = User.find_by_id(cookies.signed[:user_id])
if current_user
current_user
else
reject_unauthorized_connection
end
end
end
end
The solution is to use HTTP authorization token. It's simple, widespread and obvious. This article helped me a lot

Resources