Secure Handling of Auth Tokens with Rails - ruby-on-rails

I'm working on a new rails api project, and I'm trying to get to the best usability/security balance.
The user will establish a session by posting username/password to api/v1/sessions. For a valid user, it will create an authentication token, which is it's own activerecord model, associated by the polymorphic relationship authenticatable, giving me the flexibility to have multiple user models if I need to.
class AuthenticationToken < ActiveRecord::Base
before_create :digest_token
def self.from_authenticatable(authenticatable)
self.create(authenticatable: authenticatable, token: generate_token)
end
end
The undigested token (SecureRandom.hex) gets rendered back to the client via JSON to authenticate future requests. The token gets salted and digested for storage in the database using BCrypt and a configured work factor.
The client will provide the raw auth token and an identity (can be username or password) to load the user record. Authentication will be handled by authenticate_with_http_token
def authenticate_token
authenticate_with_http_token do |token, options|
user = User.identified_by(options["identity"])
user.authentication_tokens.any? do |auth_token|
#current_user = user if auth_token.secure_compare(token)
end
end
end
Auth tokens are compared via a constant time alogrithm lifted from devise.
If current user is not successfully set, I render 401 unauthorized.
I feel like this is reasonably secure, and the only thing I really want to do is add some more fields to the auth token to track where it was last used (user agent) to allow the user to revoke some of the auth tokens if they like.
Are there any major holes with this approach? Anything else I should consider?

Related

Authenticating docusign via Rails API (Omniauth + Devise) + JS Frontend

I'm trying to create an authentication flow using Auth Code Grant where I've added necessary omniauth strategy for Docusign to create /auth/docusign routes in Rails API only application.
Here are the steps followed
I'm issuing a request to the route from VueJS client.
window.open("http://localhost:4000/auth/docusign", "targetWindow", "width=350,height=250")
After user enters credentials and on successful login I'm calling the callback:
class SessionsController < Devise::SessionsController
def docusign
internal_destroy
#success = false
userinfo = request.env['omniauth.auth']
request_info = request.env['omniauth.params']
if userinfo
info = userinfo.info
cred = userinfo.credentials
user = User.find_by(email: info['email']) || User.find_by(id: session[:user_id])
if user
organization = user.organization
organization.organization_providers.where(provider_name: 'Docusign').destroy_all
OrganizationProvider.create(email: info['email'], token_expires_at: Time.at(cred['expires_at']), token_expires_at: Time.now, provider_name: 'Docusign', organization_id: organization.id, token: cred.token)
#success = true
end
end
render 'sessions/docusign'
end
end
I'd like to pass some params (which I'm accessing in the callback as request.env['omniauth.params']) for executing some backend tasks in the method.
When I try window.open("http://localhost:4000/auth/docusign?email='"+email+"'", "targetWindow", "width=350,height=250")
It says that the url doesn't match with any redirect urls
I have also tried passing in redirect_to('/auth/docusign', query: query) but on doing so, it doesn't open in a browser due to CORS.
I'm also trying to set it in session cookie, but since it's an API only server, I'm still working towards setting up cookie store.
Question
Which is the best way to achieve this? To pass some params in the callback and retrieve it.
Then the execution flow continues on the Rails server and the window serves a page with an appropriate response as per authentication status. However during this time, the client window which started the request is not aware of the authentication outcome.
Question
How can I communicate to the VueJS client that the authentication process is completed?
Question
Am I doing the above flow correctly or are there any better ways to achieve the same?
Thanks in advance
You need to log into your DocuSign Developer Account, Click on Admin and go on the left nav down to "API and Keys" where you can find the integration key you set. Did you set one?
If you did, you should find it and then add the redirectUri to the OAuth settings for that key (client ID in OAuth).
That is why DocuSign login tells you that the redirectURI doesn't match. You can add http://localhost:4000/auth to the list and that should work for your local env.
You cannot past custom variables on the redirectUri, it has to match exactly to the one you entered. If you need to pass values to it, there's a way to do that using state.
Here is how the URL should look, notice the &state= part of it:
https://account-d.docusign.com/oauth/auth?
response_type=code
&scope=YOUR_REQUESTED_SCOPES
&client_id=YOUR_INTEGRATION_KEY
&state=YOUR_CUSTOM_STATE
&redirect_uri=YOUR_REDIRECT_URI
&login_hint=YOUR_LOGIN_HINT
You can put whatever you want in there (URI encoded of course) and that value would come back to you when redirected back also with &state= parameter.
This solves the problem and allows you to pass arguments back to your redirect URI.

Google Oauth2 automatially renew token

I'm adding Oauth2 via Google to my Rails app using the omniauth-google-oauth2 gem. I have everything set up according to the gem's readme. When I click the authorize button in my app, I am directed to Google to authorize the app. Once I'm sent back to my redirect path, I get a hash of data that includes the token, refresh_token, and expires_at (not expires_in), however expires_at is always equal to Time.now.to_i. The hash looks something like this:
=> auth.credentials
=> {
token: 'token',
refresh_token: 'refresh_token',
expires_at: 1468603958
expires: true
}
What I would like to do is programmatically renew the user's token every time they log into the app, without forcing them to go through the initial authorization process. I have a similar token renewal flow set up for Facebook, but I can't figure out how to make this work for Google, since the token expires_at time is now.
My question is two-part:
Shouldn't the value of expires_at be in the future (e.g. 1 hour from now, or 30 days from now)?
How can I programmatically refresh the token using the refresh_token that I got when I first authorized the app? I can't seem to find a single example in Ruby on how to do this. I wanted to set up a method on my GeneralController in my app that checks the expires_at value of my Identity model and then runs some logic to renew the token, but I can't figure out how to renew the token. This also seems problematic given the expires_at value I got from Google is equal to Time.now.to_i. If I uses that value, it would trigger an infinite loop.
Here's what I want do do in pseudo code:
class GeneralController < ApplicationController
before_action :renew_google_token, only: [:home]
def renew_google_token
# Get first Identity that will soon expire. This will be repeated unti lall identities are updated
current_user.identities.where(provider: :google).where("expires_at < (?)", Time.now + 7.days).each do |identity|
# Logic that pings Google and renews the token
new_token_hash = something
identity.update(token: new_token_hash.token, expires_at: Time.at(new_token_hash.expires_at))
end
end
end

Find_for_database_authentication vs Find_by in Rails Devise app?

So, I'm trying to set up a React frontend and Rails backend with devise, and the Rails side is supposed to be an internal API. It's the first time I've ever done this, so I'm struggling with authentication. Specifically, in my SessionsController, I have this code:
def create
resource = User.find_for_database_authentication(email: params[:email])
return invalid_login_attempt unless resource
if resource.valid_password?(params[:password])
sign_in :user, resource
return render nothing: true
end
invalid_login_attempt
end
This always returns 401 Unauthorized. I check the result of calling valid_password? and it is always false.
However, if I replace find_for_database_authentication with find_by, the valid_password? works with no problems. Why is this? It's okay if for now the user can only enter his email and not his password, but this really confuses me. It also bugs me that this doesn't use any token checking (different issue).
On the side, I'm also wondering about whether or not CSRF tokens are okay for internal APIs (should I use a different token-auth?), and how I'm supposed to include a CSRF token with a login form if the user isn't logged in yet, but I guess those are questions for another post. Thanks for any help.

Rails API: Authenticate users from native mobile apps using username/password or facebook token

So I have been pulling my hair out for a few days now trying to figure out how to add username/password authentication to my rails mobile API.
Here is a brief overview of my current authentication flow:
User selects "login with Facebook" on the mobile client, client
redirects to Facebook app and requests access_token
On success, Facebook responds with the access token and the client
redirects back to my app.
The client sends the access token to my API
My API uses the koala gem to check if the access token is valid.
If the token is valid, Facebook sends the users data to the API
where a new user is created. If the user already exists, my API sends down the users data.
My API handles the access token in step 4 as shown below:
def self.authenticate_user_from_facebook(fb_access_token)
user = User.new
graph = Koala::Facebook::API.new(fb_access_token)
profile = graph.get_object('me')
#user['fb_id'] = profile['id']
#user['fb_token'] = fb_access_token
# Generate user hash
uhash = Hash.new
uhash['provider'] = 'facebook'
uhash['uid'] = profile['id']
uhash['info'] = Hash.new
uhash['info']['nickname'] = profile['username']
uhash['info']['name'] = profile['name']
uhash['info']['email'] = profile['email']
uhash['info']['first_name'] = profile['first_name']
uhash['info']['last_name'] = profile['last_name']
uhash['info']['verified'] = profile['verified']
uhash['info']['urls'] = Hash.new
uhash['info']['urls']['Facebook'] = profile['link']
uhash['credentials'] = Hash.new
uhash['credentials']['token'] = fb_access_token
uhash['extra'] = Hash.new
uhash['extra']['raw_info'] = Hash.new
#Save the new data
user = User.apply_auth(uhash)
return user
end
def self.apply_auth(uhash)
User.where(:uid => uhash['uid'], :provider => uhash['provider']).first_or_create do |user|
user.provider = uhash['provider']
user.uid = uhash['uid']
user.nickname = uhash['info']['nickname']
user.email = uhash['info']['email']
user.name = uhash['info']['name']
user.first_name = uhash['info']['first_name']
user.last_name = uhash['info']['last_name']
end
end
Once the user is created, they can make requests to my API using their access token as shown below:
In step 2 the API is using koala to verify the users access token. This is done by applying the following before_filter to all controllers.
before_filter :current_user
and in my application_helper.rb
def current_user
#current_user ||= User.authenticate_user_from_facebook(params[:access_token])
end
Every time a user makes a request to my API the koala gem is used to check if the token is valid, then the request is processed.
What I am trying to add now is authentication with only username and password.
Things I have looked into
I have been constantly referring to Railscast 235, 209, 250, 82 and reading up on OAuth2. I have a basic understanding of how authentication works but Im having trouble applying it to my current authentication flow.
Devise Token Authentication
Referring to Railscast 235, 209, and this blog post:
http://matteomelani.wordpress.com/2011/10/17/authentication-for-mobile-devices/
I can understand how to login and validate a user who logs in with a username and password. However I am confused as to how that will mesh with my Facebook login flow. I do not understand how to create a session with devise for a user who already has an access token generated by Facebook.
OAuth2
Making my API an OAuth2 provider seems like it would be a good way to go, but it seems kind of silly to redirect to a browser, and I don't know if its possible to redirect back from the browser to my app.
Authentication From Scratch
This is the option I am thinking of going with but I would be reinventing the wheel.
Thanks for reading this long post! Any advice is appreciated!
You may want to look into Warden. Warden makes it easy to setup and use different auth strategies whether you use tokens, password or Facebook. It is Rack-based so it also works outside of Rails which is nice if you ever want to use something like Grape for the API.
Here is the RailsCast on Warden. (Pro subscription required)

Using devise "rememberable" without cookies

I have a working Rails site that uses devise to manage users. For session management, I am using devise's rememberable strategy, which stores and retrieves encrypted authentication information from a user's cookie.
I'm implementing a multi-photo upload widget that uses flash. Flash does not support sending cookies along with requests. This is a problem with multiple multi-upload flash+javascript libraries, so fixing this shortcoming is probably not feasible.
So my question is: can I successfully authenticate to devise/rememberable without using cookies? And if so, how?
More details
Devise/rememberable depends on the value of remember_token within the cookie. If I could fool Rails into thinking that the value was supplied as a cookie (e.g. request.cookies['remember_token'] = '...'), my problem would be solved. Devise/rememberable would find the correct value there, unpack it, and successfully authenticate. However, the request.cookies hash is apparently read-only. Writing to the hash is silently ignored. Example (debug console from an incoming POST request):
>> request.cookies['remember_token'] = 'a string'
=> "a string"
>> request.cookies['remember_token']
=> nil
>> request.cookies
=> {}
I'm using (or trying to use) the FancyUpload v3 widget.
How about overriding Devise slightly?
Based on Devise 1.2.rc something like this should work:
module Devise
module Strategies
class Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Alternatively, you could add a new (subclassed) strategy:
module Devise
module Strategies
class RememberableParameter < Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Warden::Strategies.add(:rememberable_parameter, Devise::Strategies::Rememberable)
Or, look into Token Authenticatable:
Token Authenticatable: signs in a user based on an authentication token (also known as
"single access token"). The token can be given both through query string or
HTTP Basic Authentication
There's more about it here:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/token_authenticatable.rb
Good luck!

Resources