Monday, January 04, 2010

Authlogic: Restricting simultaneous sessions

I'm building a Rails based application for a client, which they are in-turn licensing to their customers on a per-seat basis. This means that they don't want multiple people using the same login at the same time; if you have 5 people in your shop, you have to purchase 5 licenses. Makes sense to me.

Anyway, we're using the splendid Authlogic plug-in for all the authentication duties and while it does many things very well it doesn't have a built-in way to restrict simultaneous logins. I asked around and got a couple of tips on what might work and how I might proceed. It was a simple process but after several others had the same problem I thought I would formalize the "solution" in a blog post in hopes that it might help someone else in the future.

  • Step One - Add a place to store a session key
    # add_session_key_to_users.rb
    class AddSessionKeyToUsers < ActiveRecord::Migration
    def self.up
    add_column :users, :session_key, :string
    end
    
    def self.down
    remove_column :users, :session_key
    end
    end
    
    This gives us a place to store a "session_key" value that changes every time a user logs in.
  • Step Two - Insert the "session_key" on login
    # user_sessions_controller.rb
    def create
    ...
    if @user_session.save
    # Save the session ID to detect simultaneous login attempts
    @user_session.record.session_key = session[:session_id]
    @user_session.record.save!
    ...
    end
    end
    

    What this does is forces the session_id to be saved to the User model each time a user logs into the site. We'll check this value later to make sure the session hasn't changed. I'm using the session_id here as a "unique" value but it could be anything you want; timestamp, IP address, etc...
  • Step Three - Make sure the user is unique
    # application_controller.rb
    def current_user
    ...
    # Prevent simultaneous logins
    if @current_user && @current_user.session_key != session[:session_id]
    flash[:notice] = 'Access denied. Simultaneous logins detected.'
    current_user_session.destroy 
    end
    end
    

    Here is where the rubber meets the road, so to speak. In Authlogic, the current_user method is accessed on every page request so it is the perfect place to check for duplicate user sessions. We simple verify that the session_id in the users cookie is the same one in the database. If they are different we destroy the session and update the flash message.


This is a pretty simple little hack but it seems to work OK. One "problem" that I have noticed is that while the session is immediately destroyed the current request continues unabated. This means that the two users could possibly perform two actions at the same time, but it shouldn't be a problem. Another side effect of this technique is that the last user to log in always gets the session. This might be a problem for you but it wasn't for my project.

6 comments:

Jonathan Greenberg said...

Thanks for this post! I seem to be following you by a few days in the authlogic google group.

BTW, I was able to move setting saving the user's session_id to a callback in the UserSession:

class UserSession < Authlogic::Session::Base

after_save :set_session_id

def set_session_id
record.session_id = controller.session.session_id
end
end
I discovered that the record save is not even necessary because Authlogic does it automatically. I am still learning my way around Atuhlogic so I may have missed something about doing it this way but I like that it keeps the controller skinny.

Richard Hurt said...

Ah, using a filter is even better than my solution! I love it when we all work together. :)

來吧 said...

great msg for me, thanks a lot dude˙﹏˙

Evandro Saroka said...

hi, have any of you experienced problems with the session_id length? since the migration uses :string, at least for mysql, a varchar with 255 characters is created...
we found that some session_ids are bigger than that and wanted to ask if the simple solution of making the sql field bigger is the right way to go?

thanks a lot

Rafael Santos said...

Actually, there is something you could use in authlogic, if you reset presistence token you can achieve the same behaviour. Like this:

class UserSession < Authlogic::Session::Base
before_create :reset_persistence_token

def reset_persistence_token
record.reset_persistence_token
end
end

By doing this, old sessions for a user loggin in are invalidated.

James Paden said...

Just a big thanks for posting this and for Rafael with his even simpler solution. Saved me a lot of time and trouble. Thanks!

Post a Comment