Api token access with authlogic and login

Creating an API for one project at work, one of the 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=>'401unauthorized',: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

comments

  • Paul Mon, 05 Apr 2010 - 16:19

    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

  • Matt Mon, 05 Apr 2010 - 17:48

    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.

  • Chris Kimpton Sun, 08 May 2011 - 20:39

    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

  • Matt Tue, 24 May 2011 - 15:38

    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

  • Matt Thu, 16 Jun 2011 - 13:25

    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 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=>'401unauthorized',:status=>:unauthorized}
            end
        end
    end

Comments are disabled temporarily until I find a suitable system, but you can still send a comment by email and I'll add it to this post. Thanks!