Creating an API for one project at work, one odbrhw tasks was to implement a token based authentication for some resources, but the client specifically requested not to have to handle cookies.
Also, it was requested for the user to still have to login with it’s own login and password, rather than with a permanent token, like a permanent API key.
The solution I implemented used the excellent authlogic capabilities with the single_access_token, although used slighlty differently from it’s original purpose.
Rather than keeping the single access token generated at user registration untouched, like a standard API key, I enforced it’s regeneration at both login and logout. Returned in the login response, that token then has to be provided by the client for every request that needs authentication, effectively playing the same role as a cookie.
With this solution, the client looses the ability to stay logged in by storing the credentials in the client’s machine, but as the project it’s been created for only required an API, there was no problem with that.
Implementing this solution simply puts a little big more work on the client to store and provide the token in the requests parameters, but I still found it an elegant solution to get around my problem.
The following code implements this solution in the Application and the User_Session controllers, showing the regeneration of the token in both login and logout actions with authlogic’s reset_single_access_token method.
app > controllers > application_controller
class ApplicationController > ActionController::Base
...
helper_method :check
...
def check
if current_user==nil
respond_to do |format|
format.html {redirect_to login_path} #assuming you have a named login route
format.xml {render :xml=>'<?xml version="1.0" encoding="UTF-8"?><response><status>401</status><error>unauthorized</error></response>',:status=>:unauthorized}
end
end
end
...
app > controllers > user_sessions_controller
class UserSessionsController < ApplicationController
def create
@user_session = UserSession.new(params[:user_session])
respond_to do |format|
if @user_session.save
current_user.reset_single_access_token!
format.xml
else
format.xml {render :xml=>@user_session.errors, :status=>:unauthorized}
end
end
end
def destroy
if(@user_session = UserSession.find)
current_user.reset_single_access_token!
@user_session.destroy
respond_to do |format|
format.xml {render :xml=>{:status=>'200 ok'},:status=> :ok}
end
else
respond_to do |format|
format.xml {render :xml=>@user_session.errors, :status=> :not_found}
end
end
end
end
app > models > user
class User < ActiveRecord::Base
acts_as_authentic
end
app > views > users_sessions > create.xml.builder
xml.instruct! :xml, :version=>"1.0"
xml.user{
xml.user_id(current_user.id)
xml.user_credentials(current_user.single_access_token)
}
app > controllers > users_controller
class UsersController < ApplicationController
before_filter :check
def create
end
def index
end
def update
end
def show
end
end
db > migrate > create_users
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :username
t.string :crypted_password
t.string :password_salt
t.string :persistence_token
t.string :single_access_token, :null => false
t.timestamps
end
end
def self.down
drop_table :users
end
end
Hi,
I see that in your solution there is one tricks, when user first authenticated through client and then through Web, then they cannot works in parallel, because you reset single token, maybe better, reset it only for format xml.
But also you cannot logged in parallel through two clients. Otherwise maybe better use toke of session not user’s model.
Paul
Hi Paul,
I agree with you, this solution is no where near universal and has its problems as you pointed out.
However, it has been working fine for me with two clients (a web client and an XML API client) logged in at the same, as both use different authentication methods.
The XML API uses the single_access_token, while in my application, I use the excellent authlogic, which I think uses the persistence_token of the user model. As both are different in the DB, this works well.
Also, admitting that both solutions would use the same single_access_token, resetting it only for one format would lead to strange results, and an inconsistent behavior for the user.
Now, if you want to access a website both on a web desktop and lets say on a web mobile, for sure, there will be a problem with my solution, but my guess is this will not happen often.
Hi,
Thanks for the great article (although I am coming to it late).
One query, what is:
before_filter :check
Is that an AppController method to ensure the user is logged in?
Thanks,
Chris
Hi Chris,
Sorry I didn’t see your comment comming for some reason.
I think you may have spotted something I forgot to add.
It’s been a while now, and I’ll have to dig up the code again to find that out, but as far as I remember from the top of my head, it was indeed a method in the application controller.
I’ll find that out for you.
Matt
Hi Chris, appologies for the delay in coming back to you.
I have now updated the code above with things to add in the application controller.
basically, I created a helper method in there and registered it with the helper_method call at the top of my application controller.
The code is below, and assumes you’ve got a named login route. The syntax highlighted version is up there in the post.
I hope this helps!
helper_method :check
def check401 unauthorized ‘,:status=>:unauthorized}
if current_user==nil
respond_to do |format|
format.html {redirect_to login_path} #assuming you have a named login route
format.xml {render :xml=>’< ?xml version="1.0" encoding="UTF-8"?>
end
end
end