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
Related
I developed a chrome extension using Rally's WSAPI v2.0, and it basically does the following things:
get user and project, and store them
get current iteration everytime
send a post request to create a workitem
For the THIRD step, I sometimes get error ["Not authorized to perform action: Invalid key"] since end of last month.
[updated]Error can be reproduced everytime if I log in Rally website via SSO before using the extension to send requests via apikey.
What's the best practice to send subsequent requests via apikey in my extension since I can't control end users' habits?
I did see some similar posts but none of them is helpful... and in case it helps:
I'm adding ZSESSIONID:apikey in my request header, instead of user /
password to authenticate, so I believe no security token is needed
(https://comm.support.ca.com/kb/api-key-and-oauth-client-faq/kb000011568)
url starts with https://rally1.rallydev.com/slm/webservice/v2.0/
issue is fixed after clearing cookies for
https://rally1.rallydev.com/, but somehow it appears again some time
later
I checked the cookie when the issue was reproduced, and found one with name of ZSESSIONID and its value became something else rather than the apikey. Not sure if that matters though...
code for request:
function initXHR(method, url, apikey, cbFunc) {
let httpRequest = new XMLHttpRequest();
...
httpRequest.open(method, url);
httpRequest.setRequestHeader('Content-Type', ' application\/json');
httpRequest.setRequestHeader('Accept', ' application\/json');
httpRequest.setRequestHeader('ZSESSIONID', apikey);
httpRequest.onreadystatechange = function() {
...
};
return httpRequest;
}
...
usReq = initXHR ('POST', baseURL+'hierarchicalrequirement/create', apikey, function(){...});
Anyone has any idea / suggestion? Thanks a million!
I've seen this error when the API key had both read-only and full-access grants configured. I would start by making sure your key only has the full-access grant.
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 am using Koala gem and in my UI i have an share link. How can i share the posts using the post id. Can it be done like this.
#facebook = FacebookToken.first
#graph = Koala::Facebook::API.new(#facebook.access_token)
#graph.put_object(params[:post_id], "share",:message => "First!")
It gives the following error
Koala::Facebook::ClientError: type: OAuthException, code: 240, message: (#240) Requires a valid user is specified (either via the session or via the API parameter for specifying the user. [HTTP 403]
I thing something going wrong with permission. I have added the following permission in the fave bool app
"share_item,manage_pages,publish_stream,read_stream,offline_access,create_event,read_insights, manage_notifications"
Do I need to some other permission to share a post using post id
The first parameter in put_object is not the post ID, but the ID of who is sharing it, be it a page or user.
So instead of saying:
#graph.put_object(params[:post_id] ...
You would say:
//the current user
#graph.put_object('me' ...
or
//any user that you have a UID for
#graph.put_object(#user.uid ...
or
//a page that you have post permissions for
#graph.put_object(#facebook_page.id ...
Also in a future version of Koala, put_object will be a bit different, and you should go ahead and switch over to put_connection.
I need to write a log when somebody failes to log in to my app (to track bruteforce attempts). Also I decided to log successful authentications.
So I created a SessionsController < Devise::SessionsController and tried to override the sessions#create method like that: https://gist.github.com/3884693
The first part works perfectly, but when the auth failes rails throws some kind of an exception and never reaches the if statement. So I don't know what to do.
This answer to a previous SO question - Devise: Registering log in attempts has the answer.
The create action in the devise controller calls warden.authenticate!, which attempts to authenticate the user with the supplied params. If authentication fails then authenticate! will call the devise failure app, which then runs the SessionsController#new action. Note, any filters you have for the create action will not run if authentication fails.
So the solution is to add a filter after the new action which checks the contents of env["warden.options"] and takes the appropriate action.
I tried out the suggestion, and was able to log both the successful & failed login attempts. Here is the relevant controller code:
class SessionsController < Devise::SessionsController
after_filter :log_failed_login, :only => :new
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
private
def log_failed_login
::Rails.logger.info "\n***\nFailed login with email_id : #{request.filtered_parameters["user"]}\n***\n" if failed_login?
end
def failed_login?
(options = env["warden.options"]) && options[:action] == "unauthenticated"
end
end
The log has the following entries:
For a successful login
Started POST "/users/sign_in"
...
...
***
Successful login with email_id : {"email"=>...
***
...
...
Completed 302 Found
For a failed login
Started POST "/users/sign_in"
...
...
Completed 401 Unauthorized
Processing by SessionsController#new as HTML
...
...
***
Failed login with email_id : {"email"=>...
***
...
...
Completed 302 Found
Prakash's answer is helpful, but it's not ideal to rely on SessionsController#new to be run as a side effect. I believe this is cleaner:
class LogAuthenticationFailure < Devise::FailureApp
def respond
if request.env.dig('warden.options', :action) == 'unauthenticated'
Rails.logger.info('...')
end
super
end
end
...
Devise.setup do |config|
config.warden do |manager|
manager.failure_app = LogAuthenticationFailure
end
Check out Graeme's answer if you'd prefer to hook into Warden's callbacks (Devise is implemented using Warden).
I had the same question but was unable to resolve it using the "warden.options" since, in my case, these were being cleared before redirecting to the sessions#new action. After looking into a few alternatives that I judged to be too brittle (because they involved extending some Devise classes and aliasing existing methods), I wound up using some callbacks provided by Warden. It works better for me because the callback is invoked inside the current request-response cycle and the parameters are all preserved in the env object.
These callbacks are named and appear to be designed to solve this and related problems. And they are documented!
Warden supports the following callbacks as of warden-1.2.3:
after_set_user
after_authentication (useful for logging successful sign ins)
after_fetch (alias for after_set_user)
before_failure (useful for logging failed sign ins - example below)
after_failed_fetch
before_logout
on_request
Each callback is set directly on the Warden::Manager class (may be inside config/initializers/devise.rb). To track a failed authentication attempt I added this:
Warden::Manager.before_failure do |env, opts|
email = env["action_dispatch.request.request_parameters"][:user] &&
env["action_dispatch.request.request_parameters"][:user][:email]
# unfortunately, the User object has been lost by the time
# we get here; so we take a db hit because I care to see
# if the email matched a user account in our system
user_exists = User.where(email: email).exists?
if opts[:message] == :unconfirmed
# this is a special case for me because I'm using :confirmable
# the login was correct, but the user hasn't confirmed their
# email address yet
::Rails.logger.info "*** Login Failure: unconfirmed account access: #{email}"
elsif opts[:action] == "unauthenticated"
# "unauthenticated" indicates a login failure
if !user_exists
# bad email:
# no user found by this email address
::Rails.logger.info "*** Login Failure: bad email address given: #{email}"
else
# the user exists in the db, must have been a bad password
::Rails.logger.info "*** Login Failure: email-password mismatch: #{email}"
end
end
end
I expect that you could use the before_logout callback to track logout actions as well, but I haven't tested it. There appear to be prepend_ variants of the callbacks as well.
For logout logging, you need to catch the destroy event, so add the following to the Session controller (from the above answer):
before_filter :log_logout, :only => :destroy #add this at the top with the other filters
def log_logout
::Rails.logger.info "*** Logging out : #{current_user.email} ***\n"
end
I've found another way to do this, if you want, for example, display a custom message if login fails.
In my job, if login fails we check the activity status (custom logic) and display a message, no matter if the login was correct or not.
After debug a little bit and read warden docs I know this now: Warden executes a throw(:warden, opts), so, according to ruby docs, a throw must be captured inside a catch block.
def create
flash.clear
login_result = catch(:warden) { super }
return unless login_failed?(login_result)
email = params[:user][:email]
flash[:alert] = # here I call my service that calculates the message
redirect_to new_user_session_path
end
def login_failed?(login_result)
login_result.is_a?(Hash) && login_result.key?(:scope) && login_result.key?(:recall)
end
throw docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-throw
catch docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-catch
Building on Prakash Murty's answer, I think the approach in this answer (https://stackoverflow.com/a/34816998/891359) is a cleaner way to log a succesfull login attempt. Instead of calling super, Devise offers a way to pass a block that is yielded before the view is rendered.
So instead of doing this:
class SessionsController < Devise::SessionsController
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
end
It is cleaner to do:
class SessionsController < Devise::SessionsController
def create
super do |user|
::Rails.logger.info "\n***\nSuccessful login with email_id : #{user.email}\n***\n"
end
end
end
I'm having a problem using an authlogic single access token to access a page when logout on timeout is set to true and a timeout is set.
user.rb:
acts_as_authentic do |c|
c.logged_in_timeout = 15.minutes
end
user_session.rb:
logout_on_timeout true
controller:
def single_access_allowed?
["download_xml"].include?(action_name)
end
If I try to access a page/method using the token it redirects straight away to my login page. The logout on timeout works when its turned on.
If i remove the timeout code and just have acts_as_authentic in the user.rb, the single access token works.
I want to be able to use the single access token so another application can open an xml file from my ruby on rails website.
Any ideas on what I might have done wrong and where to look to fix it and make it work?
Using authlogic 3.0.3 and rails 3.0.7.
This reply from jgdreyes last Sept 27 at https://github.com/binarylogic/authlogic/issues/64 worked for me:
I went ahead and extended Authlogic's stale? method so that it does
not see requests as stale? if accessing via single_access?. This keeps
logic for logout_on_timeout intact.
class UserSession < Authlogic::Session::Base logout_on_timeout true
def stale?
return false if single_access?
super
end
end