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
Related
So I am trying to implement dervise-jwt in a Rails 5.2 application. My login is working properly, and the tokens are being created. That being said, I want to access that same current_user from my ActionCable. Following some online tutorials, my app/channels/application_cable/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
private
def find_verified_user
if user == env['warden'].user
user
else
reject_unauthorized_connection
end
end
end
end
When I debug the code, env['warden'].user is nil. What do I have to do to get the current_user accessible via warden?
From action cable documentation I read this example for setting current_user:
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 verified_user = User.find_by(id: cookies.signed[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
I don't have user_id cookie set because authentication is made for a cas server, and it store session in Active Record.
How tell to Connection class the current user?
You need to force the session to load. Check this link here (worked for me): How force that session is loaded?
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
I have a Rails 5 API with devise_token_auth gem authentications.
Now I want personal chat for authenticated users. I do not have assets as I am using API and front is in native apps and I want native apps messaging.
So how I can authenticate users to use action cable for personal messaging using devise_token_auth gem
No cookies are generally supported in Rails 5 API. See: http://guides.rubyonrails.org/api_app.html#creating-a-new-application .
If you do a common HTTP-authentification at first somewhere at your site ( with devise_token_auth gem), then you get 3 auth headers - access_token, client, uid.
In such case you can use the Basic authentification for your Websockets connection (according https://devcenter.heroku.com/articles/websocket-security#authentication-authorization ) using these 3 auth headers:
Call (I use Chrome Simple WebSocket Client):
ws://localhost:3000/cable/?access-token=ZigtvvcKK7B7rsF_20bGHg&client=TccPxBrirWOO9k5fK4l_NA&uid=client1#example.com
Then process:
# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
params = request.query_parameters()
access_token = params["access-token"]
uid = params["uid"]
client = params["client"]
self.current_user = find_verified_user access_token, uid, client
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user token, uid, client_id # this checks whether a user is authenticated with devise
user = User.find_by email: uid
# http://www.rubydoc.info/gems/devise_token_auth/0.1.38/DeviseTokenAuth%2FConcerns%2FUser:valid_token%3F
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
This is similar to the common Websockets auth https://rubytutorial.io/actioncable-devise-authentication/
Such authentication is probably enough. I believe it is not necessary additionally to auth at the channel subscription and on every Websocket message sent to server:
http://guides.rubyonrails.org/action_cable_overview.html#server-side-components-connections
For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all the channel subscriptions that are created from there on. The connection itself does not deal with any specific application logic beyond authentication and authorization.
So if your connection is identified_by :current_user, you can later access current_user wherever inside your FooChannel < ApplicationCable::Channel! Example:
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearance_channel"
if current_user
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :on }
current_user.online = true
current_user.save!
end
end
def unsubscribed
if current_user
# Any cleanup needed when channel is unsubscribed
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :off }
current_user.online = false
current_user.save!
end
end
end
PS I you want to use cookies in Rails 5 API, you can switch it on:
http://guides.rubyonrails.org/api_app.html#other-middleware
config/application.rb
config.middleware.use ActionDispatch::Cookies
http://guides.rubyonrails.org/api_app.html#adding-other-modules
controllers/api/application_controller.rb
class Api::ApplicationController < ActionController::API
include ActionController::Cookies
...
Here the auth_headers set in the cookies by ng-token-auth are used to determine if the request is authenticated, if not the connection is refused.
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 # this checks whether a user is authenticated with devise_token_auth
# parse cookies for values necessary for authentication
auth_headers = JSON.parse(cookies['auth_headers'])
uid_name = DeviseTokenAuth.headers_names[:'uid']
access_token_name = DeviseTokenAuth.headers_names[:'access-token']
client_name = DeviseTokenAuth.headers_names[:'client']
uid = auth_headers[uid_name]
token = auth_headers[access_token_name]
client_id = auth_headers[client_name]
user = User.find_by_uid(uid)
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
I do it this way, since our React Native app sends the authentication inside the headers:
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
access_token = request.headers['access-token']
uid = request.headers['uid']
client = request.headers['client']
user = User.find_by_uid(uid)
if user && user.valid_token?(access_token, client)
logger.add_tags 'ActionCable', uid
user
else
reject_unauthorized_connection
end
end
end
end
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