Send auth_token for authentication to ActionCable - ruby-on-rails-5

module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
#puts params[:auth_token]
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.name
end
end
end
I don't use web as end point for action cable, so I want to use auth_token for authentication. By default action cable use session user id for authentication. How to pass params to connect method?

I managed to send my authentication token as a query parameter.
When creating my consumer in my javascript app, I'm passing the token in the cable server URL like this:
wss://myapp.com/cable?token=1234
In my cable connection, I can get this token by accessing the request.params:
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.name
end
protected:
def find_verified_user
if current_user = User.find_by(token: request.params[:token])
current_user
else
reject_unauthorized_connection
end
end
end
end
It's clearly not ideal, but I don't think you can send custom headers when creating the websocket.

Pierre's answer works. However, it's a good idea to be explicit about expecting these parameters in your application.
For instance, in one of your config files (e.g. application.rb, development.rb, etc...) you can do this:
config.action_cable.mount_path = '/cable/:token'
And then simply access it from your Connection class with:
request.params[:token]

Unfortunately for websocket connections, additional headers and custom ones are not supported1 by most2 websocket clients and servers.
So the possible options are:
Attach as an URL parameter and parse it on the server
path.to.api/cable?token=1234
# and parse it like
request.params[:token]
Cons: It could be vulnerable as it may end up in logs and system process information available to others that have access to the server, more here
Solution: Encrypt the token and attach it, so even if it can be seen in the logs, it would serve no purpose until its decrypted.
Attach JWT in one of the allowed parameters.
Client side:
# Append jwt to protocols
new WebSocket(url, existing_protocols.concat(jwt))
I created a JS library action-cable-react-jwt for React and React-Nativethat just does this. Feel free to use it.
Server side:
# get the user by
# self.current_user = find_verified_user
def find_verified_user
begin
header_array = self.request.headers[:HTTP_SEC_WEBSOCKET_PROTOCOL].split(',')
token = header_array[header_array.length-1]
decoded_token = JWT.decode token, Rails.application.secrets.secret_key_base, true, { :algorithm => 'HS256' }
if (current_user = User.find((decoded_token[0])['sub']))
current_user
else
reject_unauthorized_connection
end
rescue
reject_unauthorized_connection
end
end
1 Most Websocket APIs (including Mozilla's) are just like the one below:
The WebSocket constructor accepts one required and one optional
parameter:
WebSocket WebSocket(
in DOMString url,
in optional DOMString protocols
);
WebSocket WebSocket(
in DOMString url,
in optional DOMString[] protocols
);
url
The URL to which to connect; this should be the URL to which the
WebSocket server will respond.
protocols Optional
Either a single protocol string or an array of protocol strings. These
strings are used to indicate sub-protocols, so that a single server
can implement multiple WebSocket sub-protocols (for example, you might
want one server to be able to handle different types of interactions
depending on the specified protocol). If you don't specify a protocol
string, an empty string is assumed.
2 There are always excpetions, for instance, this node.js lib ws allows building custom headers, so you can use the usual Authorization: Bearer token header, and parse it on the server but both client and server should use ws.

As I already stated in a comment the accepted answer is not a good idea, simply because the convention is that the URL should not contain such sensitive data. You can find more information here: https://www.rfc-editor.org/rfc/rfc6750#section-5.3 (though this is specifically about OAuth).
There is however another approach: Use HTTP basic auth via the ws url. I found that most websocket clients allow you to implicitly set the headers by prepending the url with http basic auth like this: wss://user:pass#yourdomain.com/cable.
This will add the Authorization header with a value of Basic .... In my case I was using devise with devise-jwt and simply implemented a strategy which inherited from the one provided in the gem which pulls the jwt out of the Authorization header. So I set the url like this: wss://TOKEN#host.com/cable which sets the header to this (pseudo): Basic base64("token:") and parse that in the strategy.

In case any of you would like to use ActionCable.createCustomer. But have renewable token as I do:
const consumer = ActionCable.createConsumer("/cable")
const consumer_url = consumer.url
Object.defineProperty(
consumer,
'url',
{
get: function() {
const token = localStorage.getItem('auth-token')
const email = localStorage.getItem('auth-email')
return consumer_url+"?email="+email+"&token="+token
}
});
return consumer;
Then in case that the connection is lost it will be opened with a fresh new token.

to add to previous answers, if you used your JWT as a param, you're going to have to at least btoa(your_token) #js and Base64.decode64(request.params[:token]) #rails as rails considers dot '.' a separator so your token will be cut off #rails params side

Another way (the way I did it in the end instead of my other answer) would be to have a authenticate action on your channel. I used this to determine the current user and set it in the connection/channel. All the stuff is send over websockets so credentials are not an issue here when we have it encrypted (i.e. wss).

I was asked about it recently and want to share the solution that I currently use in production systems.
class MyChannel < ApplicationCable::Channel
attr_accessor :current_user
def subscribed
authenticate_user!
end
private
# this works, because it is actually sends via the ws(s) and not via the url <3
def authenticate_user!
#current_user ||= JWTHelper.new.decode_user params[:token]
reject unless #current_user
end
end
Then re-use warden strategies to work with that JWT (and let it handle all possible edge cases and pitfalls).
class JWTHelper
def decode_user(token)
Warden::JWTAuth::UserDecoder.new.call token, :user, nil if token
rescue JWT::DecodeError
nil
end
def encode_user(user)
Warden::JWTAuth::UserEncoder.new.call(user, :user, nil).first
end
end
Though I didn't use ActionCable for the frontend it should roughly work like this:
this.cable.subscriptions.create({
channel: "MyChannel",
token: "YOUR TOKEN HERE",
}, //...

It is also possible to pass the authentication token in the request headers and then validate the connection by accessing the request.headers hash.
For example, if the authentication token were specified in a header called 'X-Auth-Token' and your User model have a field auth_token you could do:
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.id
end
protected
def find_verified_user
if current_user = User.find_by(auth_token: request.headers['X-Auth-Token'])
current_user
else
reject_unauthorized_connection
end
end
end
end

As for security of Pierre's answer: If you're using WSS protocol, which uses SSL for encryption, then the principles for sending secure data should the same as for HTTPS. When using SSL, query string parameters are encrypted as well as the body of the request. So if in HTTP APIs you're sending any kind of token through HTTPS and deem it secure, then it should be the same for WSS. Just remember that the same as for HTTPS, don't send credentials like password through query parameters, as the URL of the request could be logged on a server and thus stored with your password. Instead use things like tokens that are issued by the server.
Also you can check this out (this basically describes something like JWT authentication + IP address verification): https://devcenter.heroku.com/articles/websocket-security#authentication-authorization.

Related

How to stop further routing in a plug?

I'm using Phoenix, and in router.ex, I have defined a function that verifies the user's JWT token, before continuing on to routing through the api, in order to protect the routes. However, I am having the issue that if, say, I return a status of 403 in the plug, it continues routing and I end up getting an error as I am both accessing the route and disallowing access.
My code looks something like this:
def validate(conn, _opts) do
if !Map.has_key?(conn.cookies, "jwt") do
conn
|> send_resp(403, "Forbidden")
# I would like to have something here that stops further
# routing - i.e. a way to say 'already sent a response; don't keep going'
end
end
Here is an excerpt from Plug.Conn.send_resp/3
Note that this function does not halt the connection, so if subsequent plugs try to send another response, it will error out. Use halt/1 after this function if you want to halt the plug pipeline.

Rails: Doorkeeper custom response from context

We are using Doorkeeper gem to authenticate our users through an API. Everything is working fine since we've implemented it few years ago, we are using the password grant flow as in the example:
resource_owner_from_credentials do |_routes|
user = User.active.find_for_database_authentication(email: params[:username])
if user&.valid_password?(params[:password])
sign_in(user, force: true)
user
end
end
Doorkeeper is coupled with Devise, which enable reconfirmable strategy. As you can see in the code above, we are only allowing active users (a.k.a users with a confirmed email) to connect:
User.active.find_.....
Problem
Our specifications changed and now we want to return a different error on login (against /oauth/token) depending if the user has confirmed its email or not.
Right now, if login fails, Doorkeeper is returning the following JSON:
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}
Ideally, we want to be able to return a custom description if and only if the current email trying to login is unconfirmed
We've checked the documentation on Doorkeeper but it does not seems to have an easy way (if any at all) to do this. The fact that resource_owner_from_credentials method is located in the config adds too much magic and not enough flexibility.
Any ideas ?
Ok so after digging a little bit, we found an easy way to work around this issue by overriding Doorkeeper::TokensController.
# frozen_string_literal: true
class TokensController < Doorkeeper::TokensController
before_action :check_if_account_is_pending, only: :create
private
def check_if_account_is_pending
user = User.find_by(email: params['username'])
render json: unconfirmed_account_error if user && !user.confirmed?
end
def unconfirmed_account_error
{ error: 'invalid', error_description: 'You must validate your email address before login' }
end
end
We also needed to make sure the routes were pointing to the custom controller:
use_doorkeeper do
controllers tokens: 'tokens'
end
Hope it can helps someone in the future

How to use Pundit with Actioncable (Rails 5)

I am wondering how to limit the connection to a channel or the streaming of messages over a channel in rails5. Currently I groups with users in the groups working with pundit and the connection to the websocket happens within that group. If a malicious user randomly guessed groups they could potentially read a message over a socket they shouldn't.
When you create a new message the following code is run in my controller:
if message.save
ActionCable.server.broadcast(
"messages_{message.groupchat_id}_channel",
message: message.content,
user: message.user.email
)
head :ok
end
I have no idea what I'm doing.
Here's the solution I found to use Pundit with Activecable.
First we need access to the user model. You can do that by following the instructions in the Action Cable Overview - Connection Setup. Mainly you need to change the code in connection.rb
# 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 verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Note: you may need to use a different way of finding your user. I'm using devise_token_auth and so needed to pass the uid, token, and client_id to the connection.rb and then got the user via this code:
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
I mention this just because how you get your user may vary. The main thing is that you need to use identified_by current_user and set it.
Another thing which I did not immediately find in the documentation, is that the current_user is now accessible by your channels. Since the user name may be different than your pundit_user name, I found it easiest to manually pass the user to Pundit at that point. So in subscribing with my channel file, I had this code:
def subscribed
message = MessagePolicy::Scope.new(self.current_user, Project).resolve.find(params[:message])
stream_for message
end
You could of course also manually authorize this way, instead of using Scope:
MessagePolicy.new(self.current_user, message).show?
You can add another layer of security for actioncable connections in app/channels/application_cable/connection.rb file.
You can follow this tutorial. I think this give you some idea:
https://www.learnenough.com/action-cable-tutorial#sec-login_protection

Authenticating stomp web socket clients

I have a ruby on rails application deployed to torquebox. I need some way to secure the websockets in my application. I am using the stomp websockets , is there a way to authenticate users while they make a websocket connection? I could use the username and password parameters but they are currently ignored. Is there any other way to authenticate this connection? Thanks!
You can authenticate a message to a Stomplet by using the session and a stored token. For this to work, you have to setup Rails to use the Torquebox session store. This can be done with an initializer, such as config/initializers/torquebox_init.rb:
AppName::Application.config.session_store :torquebox_store
Now the Stomplet will have access to the session. Here is an example Stomplet that uses the session param :authentication_token to match the User's authentication_token in the database. The auth token is checked for subscribing, sending a message, and unsubscribing:
require 'torquebox-stomp'
class StompletDemo
def initialize()
super
#subscribers = []
end
def configure(stomplet_config)
end
def on_message(stomp_message, session)
token = session[:authentication_token]
if is_authenticated?( token )
#subscribers.each do |subscriber|
subscriber.send( stomp_message )
end
end
end
def on_subscribe(subscriber)
session = subscriber.session
if is_authenticated?(session[:authentication_token])
#subscribers << subscriber
end
end
def on_unsubscribe(subscriber)
session = subscriber.session
if is_authenticated?(session[:authentication_token])
#subscribers.delete( subscriber )
end
end
def is_authenticated?(token)
User.where( authentication_token: token ).exists?
end
end
Now all you have to do is make sure that when the user authenticates, the session[:authentication_token] is set. Mostly like this will be set in a controller:
# user has successfully authenticates
session[:authentication_token] = #user.authentication_token
For other people having this issue, this is how I solved it.
https://gist.github.com/j-mcnally/6207839
Basically the token system didnt scale for me, especially since I use devise.
If you want to host your websocket in say a chrome extension its easier to just pass username/password directly to stomp and have it manage its own virtual subscriber sessions in the stomplet. This also allow you to do some fun things as far as who you are pushing to.

Rails metal http basic auth

I am trying to use HTTP basic authentication when a remote service hits one of my Rails metal endpoints.
I am wondering how to get access to the username and password of the request. So far, I have printed out the entire env object and can't find the username or password anywhere in that object.
Instead, I see
"HTTP_AUTHORIZATION"=>"Basic YW5yZ3JpZDpzYWZlbWFpbA==", "PASSENGER_CONNECT_PASSWORD"=>"U4vAn6lVAOe2C8nIQSWT93j3SFJkA5VxOicSeDspF9a"
But I am not sure what to do with this.
Ideally, I'd like to do something like:
class MyHook
def self.call(env)
user = env[...]
pass = env[...]
if (user == "foo" && pass == "pass")
# do stuff
end
end
end
Thanks!
require 'base64'
# Decode the colon-separated, base64-encoded user and password after "Basic "
user, pass = Base64.decode64(env['HTTP_AUTHORIZATION'].split[1]).split ':', 2
I would definitely retire the password you posted encoded with your question. :-)