Rails 5 ActionCable error description and rejected connection - ruby-on-rails

I made a basic chat with ActionCable authenticated with devise.
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.email
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
end
But when the user has an open chat and it rejects the connection (because the users have logged out), I need to show a login screen.
The problem is that on the frontend I can't get the reason for the disconnection.
How can I send reject with params, like "unauthorized"?

def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
message = "The user is not found. Connection rejected."
logger.add_tags 'ActionCable', message # to console
self.transmit error: message # this is what you wanted
reject_unauthorized_connection
end
end
See also: How to terminate subscription to an actioncable channel from server?

Related

Access env['warden'].user from ApplicationCable is nil

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?

How retrieve session store from inside ActionCable::Connection::Base

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?

Making current_user available in ActionCable channels using bcrypt

I'm trying to make current_user available in my channel actions.. This is the first time I'm diving into ActionCable and the tutorial I'm following is using Devise and I am not..
In channels/application_cable/connection.rb. They have
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.email
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
end
I'm having trouble with the protected method. I'm not sure how to verify the user from this file using bcrypt.. Also the directory this file is under seems to be likened to helpers for ActionCable, am I correct in thinking this?

env['warden'] not working with Rails 5

Im following this guide to create Chatting feature with a use of Websockets.
https://www.sitepoint.com/rails-and-actioncable-adding-advanced-features/
Im stuck with a problem that env['warden'].user is retuning nothing even when Im loggined to the app with standard Devise form.
And if I use another method (which is commented now) - it return wrong user
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.email
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
verified_user = env['warden'].user
if verified_user
verified_user
else
reject_unauthorized_connection
end
end
# def find_verified_user
# user_id = request.headers['HTTP_AUTHORIZATION']
# if verified_user = User.find_by(user_id)
# verified_user
# else
# reject_unauthorized_connection
# end
# end
end
end
Logs says
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2017-04-06 17:40:17 +0300
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
An unauthorized connection attempt was rejected
Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
I had the same problem, but it turned out my issue was because I had two Devise models: User and Customer, since User was used for admin stuff, Customer was set to Warden's default_scope. So to access the user scope, I had to do the following
env['warden'].user(:user)
The symbol at the end defines the scope to use.
Here's where I found information on Warden's Scoped Users: https://github.com/wardencommunity/warden/wiki/Scopes#scoped-user-access
I found solution on this article
https://rubytutorial.io/actioncable-devise-authentication/
Im not sure how it works, but it does the deal. How it would help for people with similar problem.
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.email
end
protected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now
verified_user
else
reject_unauthorized_connection
end
end
end
end
And I also created /config/initializers/warden_hooks.rb file
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
auth.cookies.signed["#{scope}.expires_at"] = 60.minutes.from_now
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil
end
Using Devise for authentication, I just ran into the same problem and the accepted answer didn't help me. My mistake was in my routes.rb file.
I placed: mount ActionCable.server, at: '/cable'
inside the authenticate :user do block. Moving it into the: Rails.application.routes.draw do block resolved the issue for me.

How To use devise_token_auth with ActionCable for authenticating user?

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

Resources