Multiple token authentication with Devise & redis


I’ve been developing on Rails for quite some time now and i continue to realize the power and benefits this framework gives you. In Rails, when you want to get a new capability into your application, you first try to find something OOTB. The best gem for the job is usually the one that gives you everything you want in one place. The variety and maturity of community plugins provide Rails developers with the ability to easily create rich web applications. But what happens when you find a plugin that gives you 95% of what you want and you need to add the 5% yourself ?
I found Devise to be that plugin. This great plugin gave me everything i needed in order to authenticate users in my application except one feature.
Using Rails, i have developed an api and i was looking for a way to authenticate users in different clients simultaneously and let every client work dependently of the others. Devise only gives you 2 forms of authentication: database (using cookies) and token based authentication. I wanted to use the database authentication for users who login with email and password and the token authentication for those who wanna use the api from mobile devices and other applications and want to just login with a token.
Database authentication using Devise was pretty easy. With the tokens i had 2 requirements:
- The token had to be regenerated on every login (for security reasons).
- Logging out of the api had to revoke the token.
The problem i had was that logging in from 2 devices simultaneously was impossible because (the numbers correlate to the requirements numbers above):
- One client login always revokes the token for the other.
- Logging out from one client revokes the token for the other.
I figured i had to generate a token for each user+client. That token will “live” for as long as the user is logged in on a specific client. In order to accomplish that and still use Devise i had to expand its capabilities to support my multiple tokens philosophy. I also selected redis as my storage for the tokens (I won’t get down to details on “why redis” in this post)
So lets go down to the code:
First, i had to create a class that handles the interaction with redis. I created a file under “lib” and called it authentication_tokens.rb. In my implementation, every token is added to redis with the prefix “auth_token:”. You can also notice that i’ve put a 24.hours expiration time for a token (24 hours of no activity with the token).
class AuthenticationTokens
include Singleton
def initialize
@redis ||= Redis.new(:host => "localhost")
end
def AuthenticationTokens.finalize(id)
@redis.quit
end
# generate authentication token using the given email and save it in redis
# the generated token will have a field "user_id" in order to identify the associated user later
def generate(email, user_id)
token = Utils::gen_token(email)
key_s = token_key token
@redis[key_s] = user_id
@redis.expire key_s, 24.hours
token
end
# renewing expiration timeout
def touch(token)
@redis.expire token_key(token), 24.hours
end
# deletes the token
def revoke(token)
@redis.del token_key(token)
end
def token_val(token)
@redis.expire token_key(token), 24.hours
@redis[token_key(token)]
end
private
def token_key(token)
"auth_token:#{token}"
end
end
I also created a Devise strategy to handle my multiple tokens authentication logic inside devise. The strategy is kept in multiple_tokens_strategy.rb under “config/initializers”. The strategy uses the previously created class in order to check with redis if the token exists.
module Devise
module Strategies
class MultipleTokensStrategy < Devise::Strategies::Base
def valid?
params[:auth_token]
end
def authenticate!
user_id_str = AuthenticationTokens.instance.token_val(params[:auth_token])
if user_id_str
user = User.find(user_id_str.to_i)
if user
user.after_database_authentication
success!(user)
end
end
if (!(params[:user]) && !halted?)
fail!("Invalid authentication token.")
end
end
end
end
end
The newly created strategy needs to be added to the initializer “devise.rb”
Devise.setup do |config|
...
config.warden do |manager|
manager.strategies.add(:multiple_tokens_strategy, Devise::Strategies::MultipleTokensStrategy)
manager.default_strategies(:scope => :user).unshift :multiple_tokens_strategy
end
end
After creating the strategy and registering it with Devise we just need to generate the token when the user logs into the system (through the api or regular signin). In order to do that we’ll have to override Devise’s SessionsController as explained in many places on the web (for example: http://stackoverflow.com/questions/8070320/rails-3-override-devise-sessions-controller). After overriding Devise’s SessionsController, i overridden 3 actions: “create” & “require_no_authentication” & “destroy”. I copied Devise’s original code and added my functionality inside. Inside the code you’ll see comments with what i did exactly:
class SessionsController resource_name,
:recall => ("#{controller_path}#new" : "sessions#authentication_failure_json"))
# This code is copied from Devise
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource)
# if the winning strategy is MultipleTokensStrategy than don't replace
# auth_token, otherwise generate a token for the user.
if warden.winning_strategy.class != Devise::Strategies::MultipleTokensStrategy
auth_token = AuthenticationTokens.instance.generate resource.email, resource.id
else
# we assume that params[:auth_token] exists b/c winning strategy is MultipleTokensStrategy
auth_token = params[:auth_token]
end
# keep the token in the session for future reference
session[:auth_token] = auth_token
# Here i respond to different formats.
respond_to do |format|
format.html { redirect_to root_path }
format.json { render :json => resource }
end
end
# This action is called to check if an authentication is actually needed.
def require_no_authentication
# This code is copied from Devise
no_input = devise_mapping.no_input_strategies
args = no_input.dup.push :scope => resource_name
if no_input.present? && warden.authenticate?(*args)
resource = warden.user(resource_name)
flash[:alert] = I18n.t("devise.failure.already_authenticated")
# My code here
# We generate a new token even if no authentication is required b/c we don't
# know what token to return if the user is logged in already and asks to
# login again ONLY with user and password than (in my implementation) he
# actually "asks" for a new token.
# If the user is logged in already and asks to login again with
# a valid auth_token than that token is returned.
auth_token = params[:auth_token] || session[:auth_token] || AuthenticationTokens.instance.generate(resource.email, resource.id)
session[:auth_token] = auth_token
# Here i respond to different formats.
respond_to do |format|
format.html { redirect_to after_sign_in_path_for(resource) }
format.json { render :json => resource }
end
end
end
# This action is called on logout.
def destroy
# My code here.
# I look for the user with the given token. If he exists than he gets logged
# out, if not than the current_user is logged out.
auth_token = params[:auth_token]
if auth_token
user_id_str = AuthenticationTokens.instance.token_val(auth_token)
if user_id_str.blank?
if current_user
@user = current_user
else
respond_to do |format|
format.html { redirect_to root_path }
format.json { render :json => {:errors => "Already logged out!"} }
end
return
end
else
@user = User.find(user_id_str.to_i)
AuthenticationTokens.instance.revoke auth_token
session[:auth_token] = nil
end
else
@user = current_user
end
# This code is copied from Devise
sign_out(@user)
# Here i respond to different formats.
respond_to do |format|
format.html { redirect_to root_path }
format.json {
render :status => 200, :json => { :email => @user.email } }
end
end
# This function supports returning error message on failure to authenticate.
def authentication_failure_json
return render :json => { :errors => alert }
end
end
That’s it! Quite an effort to get it done but with that you’ll have the ability to connect with multiple clients to your rails application without then interfering each other.
