Devise limit one session per user at a time

23,567

Solution 1

You can't do it.

  • You can control IP addresses of user, so you can prevent presence of user from two IP at a time. ANd you can bind login and IP. You can try to check cities and other geolocation data through IP to block user.
  • You can set cookies to control something else.

But none of this will guarantee that only one user uses this login, and that those 105 IP from all over the world doesn't belong to only one unique user, which uses Proxy or whatever.

And the last: you never need this in the Internet.

UPD

However, what I'm asking is about limiting multiple users from using the same account simultaneously which I feel should be possible

So you can store some token, that will contain some encrypted data: IP + secret string + user agent + user browser version + user OS + any other personal info: encrypt(IP + "some secret string" + request.user_agent + ...). And then you can set a session or cookie with that token. And with each request you can fetch it: if user is the same? Is he using the same browser and the same browser version from the same OS etc.

Also you can use dynamic tokens: you change token each request, so only one user could use system per session, because each request token will be changed, another user will be logged out as far as his token will be expired.

Solution 2

This gem works well: https://github.com/devise-security/devise-security

Add to Gemfile

gem 'devise-security'

after bundle install

rails generate devise_security:install

Then run

rails g migration AddSessionLimitableToUsers unique_session_id

Edit the migration file

class AddSessionLimitableToUsers < ActiveRecord::Migration
  def change
    add_column :users, :unique_session_id, :string, limit: 20
  end
end

Then run

rake db:migrate

Edit your app/models/user.rb file

class User < ActiveRecord::Base
  devise :session_limitable # other devise options
  ... rest of file ...
end

Done. Now logging in from another browser will kill any previous sessions. The gem actual notifies the user that he is about to kill a current session before logging in.

Solution 3

This is how I solved the duplicate session problem.

routes.rb

  devise_for :users, :controllers => { :sessions => "my_sessions" }

my_sessions controller

class MySessionsController < Devise::SessionsController
  skip_before_filter :check_concurrent_session

  def create
    super
    set_login_token
  end

  private
  def set_login_token
    token = Devise.friendly_token
    session[:token] = token
    current_user.login_token = token
    current_user.save(validate: false)
  end
end

application_controller

  def check_concurrent_session
    if duplicate_session?
      sign_out_and_redirect(current_user)
      flash[:notice] = "Duplicate Login Detected"
    end
  end

  def duplicate_session?
    user_signed_in? && (current_user.login_token != session[:token])
  end

User model Add a string field via a migration named login_token

This overrides the default Devise Session controller but inherits from it as well. On a new session a login session token is created and stored in login_token on the User model. In the application controller we call check_concurrent_session which signs out and redirects the current_user after calling the duplicate_session? function.

It's not the cleanest way to go about it, but it definitely works.

Solution 4

As far as actually implementing it in Devise, add this to your User.rb model. Something like this will log them out automatically (untested).

  def token_valid?
     # Use fl00rs method of setting the token
     session[:token] == cookies[:token]
  end

  ## Monkey Patch Devise methods ##
  def active_for_authentication? 
    super && token_valid?
  end 
  def inactive_message 
   token_valid? ? super : "You are sharing your account." 
  end 

Solution 5

I found that the solution in the original posting did not quite work for me. I wanted the first user to be logged out and a log-in page presented. Also, the sign_out_and_redirect(current_user) method does not seem to work the way I would expect. Using the SessionsController override in that solution I modified it to use websockets as follows:

def create
  super
  force_logout
end

private
def force_logout
    logout_subscribe_address = "signout_subscribe_response_#{current_user[:id]}"
    logout_subscribe_resp = {:message => "#{logout_subscribe_address }: #{current_user[:email]} signed out."}
    WebsocketRails[:signout_subscribe].trigger(signout_subscribe_address, signout_subscribe_resp)
  end
end

Make sure that all web pages subscribe to the signout channel and bind it to the same logout_subscribe_address action. In my application, each page also has a 'sign out' button, which signs out the client via the devise session Destroy action. When the websocket response is triggered in the web page, it simply clicks this button - the signout logic is invoked and the first user is presented with the sign in page.

This solution also does not require the skip_before_filter :check_concurrent_session and the model login_token since it triggers the forced logout without prejudice.

For the record, the devise_security_extension appears to provide the functionality to do this as well. It also puts up an appropriate alert warning the first user about what has happened (I haven't figured out how to do that yet).

Share:
23,567
John
Author by

John

Updated on July 09, 2022

Comments

  • John
    John almost 2 years

    My app is using Rails 3.0.4 and Devise 1.1.7.

    I'm looking for a way to prevent users from sharing accounts as the app is a subscription based service. I've been searching for over a week, and I still don't know how to implement a solution. I'm hoping someone has implemented a solution and can point me in the right direction.

    Solution (Thank you everyone for your answers and insight!)

    In application controller.rb

    before_filter :check_concurrent_session
    
    def check_concurrent_session
      if is_already_logged_in?
        sign_out_and_redirect(current_user)
      end
    end
    
    def is_already_logged_in?
      current_user && !(session[:token] == current_user.login_token)
    end
    

    In session_controller that overrides Devise Sessions controller:

    skip_before_filter :check_concurrent_session
    
    def create
      super
      set_login_token
    end
    
    private
    def set_login_token
      token = Devise.friendly_token
      session[:token] = token
      current_user.login_token = token
      current_user.save
    end
    

    In migration AddLoginTokenToUsers

    def self.up
      change_table "users" do |t|
        t.string "login_token"
      end
    end
    
    def self.down
      change_table "users" do |t|
        t.remove "login_token"
      end
    end
    
  • John
    John over 12 years
    I'm more worried about users sharing an account within an organization than across countries. Will uniq IP solution be viable if the account being shared is by multiple people on the same network?
  • Marc B
    Marc B over 12 years
    Possibly, unless they've got a NAT gateway, which'd make all the users appear under a single (or very few IPs).
  • John
    John over 12 years
    Sharing the account I agree is impossible to stop. However, what I'm asking is about limiting multiple users from using the same account simultaneously which I feel should be possible.
  • John
    John over 12 years
    Hi, thanks your response. I'm trying to fully understand your solution combined with floors. I didn't find active_for_authentication? but I did find active? which I believe serves the same purpose. I'm a little lost where the cookies[:token] comes into play? Are you just setting the token in the cookie or are you storing it in a database. I have updated my question to reflect my current progress. Thanks.
  • John
    John over 12 years
    Thanks for your update fl00rs. I'm using a simple token for now until I get it working, but I appreciate your detailed response.
  • Kenny Meyer
    Kenny Meyer over 12 years
    You never can't do it == You can do it. Double negation here.
  • Mason
    Mason over 10 years
    It looks like it has that feature, but as of when I wrote this, it isn't compatible with Rails 4 and Ruby 2.0
  • Jon Cairns
    Jon Cairns over 9 years
    @KennyMeyer not only that, it means that it's impossible to not be able to do it :)
  • Matthew
    Matthew over 8 years
    Works for me on Rails 4.2 && ruby 2.2.0
  • Kenny Meyer
    Kenny Meyer over 7 years
    I wonder if you still have the same opinion about this now @fl00r ?
  • fl00r
    fl00r over 7 years
    @KennyMeyer yes. There are workarounds which are suitable in many cases, but in general, you can't guarantee access for only one real person under one account.
  • Taha Rushain
    Taha Rushain over 6 years
    Works on Rails 5.0.2 and Ruby 2.3.4. I want to refresh the previous login page after a new login, how can I achieve that?
  • scarver2
    scarver2 over 6 years
    @TahaRushain You would not use session_limitable gem to refresh a previous session. Can you elaborate why you would want to refresh a previous session if one is accessing a site from a new browser? More detail is needed to properly answer your question.
  • Taha Rushain
    Taha Rushain over 6 years
    @scarver2 I think I didn't explain myself correctly, I don't want to refresh the session but the view so that as soon as a new session is created in another browser, the older browser should redirect to sign-in or root path in real-time. This type of flow would provide a better UX. Would that be possible without using ActionCable?
  • scarver2
    scarver2 over 6 years
    @TahaRushain Thank you for clarifying. A good UX is admirable. With a little help from AJAX, just poll an endpoint every n minutes. If you get a 200 OK, you're still logged in. If you get a 301 or 302 Redirect, then flash a message or initiate the redirect in the old browser.
  • Parthiv
    Parthiv over 6 years
    Can we manage two different sessions in multiple tabs on same browser?
  • Saqib Shahzad
    Saqib Shahzad over 3 years
    how can I notify the user that he is about to kill other sessions?