How To use devise_token_auth with ActionCable for authenticating user?
Asked Answered
B

3

5

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

Boles answered 28/9, 2016 at 7:46 Comment(0)
T
9

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&[email protected]

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
...
Thedathedric answered 7/4, 2017 at 13:0 Comment(3)
How do you get the token in cable.js file? App.cable = ActionCable.createConsumer('/cable/' + token); I can't figure how to get the token from the user.Macro
There is no cable.js file here since this is a Rails API-only case: guides.rubyonrails.org/api_app.html . Your problem is described here: #13827094Thedathedric
Will this keep working with rotating tokens, i.e tokens changing on each request for added security, obv i cannot re-open a ws connection everytime token changesSmoot
Y
5

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
Yordan answered 14/10, 2016 at 13:17 Comment(4)
When authenticating by token from a native app, is there suppose to be any cookies set in the API server ??Rashidarashidi
@Rashidarashidi the code you see above is from a backend that connects to a hybrid cordova application. The cordova app uses ng-token-auth which does set cookies. I guess that for a native app you would send the auth data in the headers of each request. But still the flow would be quite similar.Yordan
@quintencis, The problem is that for websockets you can't send custom headers other than the specified by the ws protocol, which of course does not include uid, access-token or client :(. I ended up sending the token, uid and client as query strings in the url, which is quite insecure.Rashidarashidi
@Rashidarashidi There is some good info on the subject over here: devcenter.heroku.com/articles/…Yordan
I
0

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

Incoordinate answered 30/12, 2022 at 15:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.