Devise limit one session per user at a time

Ruby on-RailsRuby on-Rails-3SessionDevise

Ruby on-Rails Problem Overview


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

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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 2 - Ruby on-Rails

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 3 - Ruby on-Rails

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 - Ruby on-Rails

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 - Ruby on-Rails

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).

Solution 6 - Ruby on-Rails

Keep track of uniq IPs used per user. Now and then, run an analysis on those IPs - sharing would be obvious if a single account has simultaneous logins from different ISPs in different countries. Note that simply having a different IP is not sufficient grounds to consider it shared - some ISPs use round-robin proxies, so each hit would necessarily be a different IP.

Solution 7 - Ruby on-Rails

While you can't reliably prevent users from sharing an account, what you can do (I think) is prevent more than one user being logged on at the same time to the same account. Not sure if this is sufficient for your business model, but it does get around a lot of the problems discussed in the other answers. I've implemented something that is currently in beta and seems to work reasonably well - there are some notes here

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJohnView Question on Stackoverflow
Solution 1 - Ruby on-Railsscarver2View Answer on Stackoverflow
Solution 2 - Ruby on-Railsfl00rView Answer on Stackoverflow
Solution 3 - Ruby on-RailsnulltekView Answer on Stackoverflow
Solution 4 - Ruby on-RailsDexView Answer on Stackoverflow
Solution 5 - Ruby on-RailsdavidmView Answer on Stackoverflow
Solution 6 - Ruby on-RailsMarc BView Answer on Stackoverflow
Solution 7 - Ruby on-RailschrispandaView Answer on Stackoverflow