Consider a simple application where a user fills a form to divide two numbers, in the routes the form data is proceeded [made into float] and then passed as parameters to a python script's function that has the division logic.
The logic fails due to division by 0 is handled as a custom message in the terminal. How does one send this custom message back to the front end UI along with a 500 error message? Trying to make a restful flask app here.
So far I can abort and show a custom message but not the one that propagated from the backend. Also looked into custom error handling but I want to writer of the external python script to be able to write the custom message.
You can Flask errorhandler(errorcode) to manage your errors and display those on the frontend.
#app.errorhandler(500)
def code_500(error):
return render_template("errors/500.html", error=error), 500
You can put whatever else you want in the html template.
You can also call the code_500(error) func directly.
Same principle applies for any other HTTP error code if you want to customize the page and the message (401, 403, 404, etc...).
If you're inside a blueprint, you can use app_errorhandler instead.
You could use the abort() function. From the docs:
When using Flask for web APIs, you can use the same techniques as above to return JSON responses to API errors. abort() is called with a description parameter. The errorhandler() will use that as the JSON error message, and set the status code to 404.
You could implement it like this
#app.route("/div")
def divide():
x, y = request.form['x'], request.form['y']
try:
result = x / y
except ZeroDivisionError:
abort(400, description="Your message here")
else:
# Proper response
From there, the important step is properly catching that message on your frontend.
I have one POST call related to search.It is like I am sending some data as parameters to call and some in payload.after getting 302 it gets redirected.But the issue is once it gets redirected,POST call gets converted to GET call and payload is lost.As a result I am unable to get desired search result.Is there anything related to config that I might be missing??
Yes this is the correct behavior. Sounds like you need to disable automatic re-directs for this test, see configure. You can do:
* configure followRedirects = false
And then get the redirect location manually as follows:
* def location = responseHeaders['Location'][0]
Refer to this test for an example: redirect.feature
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.
I'm working on a Haskell Snap-based web app, and I want to expose an API endpoint that will be invoked by a remote service without establishing an authenticated session a-priori; however, I do want that request to be authenticated, so the credentials should be provided at the time of the request.
You could imagine the request containing four fields:
username
password
payload id
payload file
The payload id and file might be irrelevant for this question, but I include them because I (a) need to support file uploads in this request (which, as I understand it, restricts the encoding used to send fields) and (b) need to retrieve at least one non-file field. The combination of those things posed some difficulty when I set this up without authentication, so perhaps it is relevant.
In Snap parlance, let's call this handler uploadHandler.
As indicated above, I have this working fine without authentication, with a setup like this:
uploadHandler :: Handler App App ()
uploadHandler = do
-- collect files / form fields and process as needed.
-- and using the routes:
routes :: [(ByteString, Handler App App ())]
routes = [ ("/login", with auth handleLoginSubmit)
, ("/logout", with auth handleLogout)
, ("/new_user", with auth handleNewUser)
-- handle the upload:
, ("/upload", handleUpload)
]
The naive solution is to simply add 'with auth' and change the type of handleUpload:
uploadHandler :: Handler App (AuthManager App) ()
uploadHandler = do
-- collect files / form fields and process as needed.
-- and using the routes:
routes :: [(ByteString, Handler App App ())]
routes = [ ("/login", with auth handleLoginSubmit)
, ("/logout", with auth handleLogout)
, ("/new_user", with auth handleNewUser)
-- handle the upload, with auth:
, ("/upload", with auth handleUpload)
]
However, this seems to require two requests: (i) authenticate and establish a session, (ii) send the POST request containing the actual payload.
I found a way to do this in one request, but it seems like there should be a more elegant means. Here's the example restricted POST handler I've hacked together:
restrictedPOST :: Handler App (AuthManager App) ()
restrictedPOST = do
mName <- getPostParam "username"
mPass <- getPostParam "password"
let uName = C8.unpack $ fromMaybe "" mName
pass = ClearText $ fromMaybe "" mPass
authResult <- loginByUsername (T.pack uName) pass False
case authResult of
Left authFail -> writeText "Could not log in"
Right user -> writeText (T.append "Hello " (userLogin user))
Is there something like 'with auth' that I can use instead of turning this example (restrictedPOST) into a combinator? I realize it may need to know which fields to get credentials out of, but I also know very little about web services (maybe there is another means? Maybe this is a total non-issue, and I just don't know how to check auth for POST requests. I'm open to any suggestions!)
I don't think you understand what with auth is doing. It has nothing to do with whether authentication is required. All it does is convert a Handler b (AuthManager b) into a Handler b v. No permissions checks are performed. Your restrictedPOST function has the right idea.
I've implemented API caching based on http://robots.thoughtbot.com/caching-api-requests. I'm using memory as the storage. How can I reset the cache manually without restarting the server?
I've tried using Rails.cache.clear, but it doesn't seem to work. The data is still getting pulled from the cache. I checked it by observing the server log for my puts message (as shown below).
Caching code:
module Meh
class Api
include HTTParty
#...
cache_name = options[:path] + "/" + options[:params].values.join(",")
response = nil
APICache.get(cache_name, cache: 3600) do
response = self.class.get options[:path], query: options[:params]
# For future debugging
puts "[API] Request: #{response.request.last_uri.to_s}"
# Just return nil if there's an error with the request, for now
if response.code == 200
response.reverse!
else
response = nil
end
end
end
Have you tried 'rake tmp:cache:clear' or deleting the contents of tmp/cache/ manually?
Are you trying to delete the contents of the cache from within the code?
Reading through the api_cache gem, it looks like this is a memory cache, not a file cache. Which would be consistent with your reports. It also looks like there is a .delete method on the APICache api. link So APICache.delete(cache_name) may be what you are looking for.