Limiting number of simultaneous logins (sessions) with Rails and Devise? [duplicate] - ruby-on-rails

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

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.

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.

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.

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

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

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.

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

Related

Devise - bypass_sign_in without active_for_authentication? callback

I have functionality of inactive account in my application for handling this i override active_for_authentication? method as below
def active_for_authentication?
super && activated?
end
But In my application super admin can also directly login in to other user account, whether it is active or not active
bypass_sign_in(User.find(resource.id))
I used above method for by pass sign in, it allows me to directly sign in only for activated user, when i login for non activated user it goes in infinite loop .
Any solutions to over come this issue or don't run active_for_authentication? callback when bypass_sign_in?
When admin logs in to another user account you can store some additional data in session, that makes it clear that this is the super admin mode.
def login_as(another_user)
return unless current_user.super_admin?
session[:super_admin_mode] = true
bypass_sign_in(another_user)
end
Unfortunately, you can't access session in Rails models, but you can store needed session information in some per-request global variable that is available in models. The solution might be like this:
module SessionInfo
def self.super_user_mode?
!!Thread.current[:super_user_mode]
end
def self.super_user_mode=(value)
Thread.current[:super_user_mode] = value
end
end
In the ApplicationController:
class ApplicationController < ActionController::Base
before_filter :store_session_info
private
def store_session_info
SessionInfo.super_user_mode = session[:super_admin_mode]
end
end
In the model:
def active_for_authentication?
super && (activated? || SessionInfo.super_user_mode?)
end
Also, you should make sure that the :super_admin_mode flag is removed from session when the super user logs out. Maybe it happens automatically, I am not sure. Maybe you will need to do it manually overriding Devise::SessionsController#destroy method (see the example below)
def destroy
session[:super_admin_mode] = nil
super
end
Also read this for better understanding of how devise handles session Stop Devise from clearing session
I recently came across a similar issue where I needed to allow an Admin to sign in as regular Users who were not active in Devise. I came up with the following solution that doesn't involve using Thread.current (which after looking into further online it seems like using Thread.current could be a precarious solution to this problem).
You can create a subclass of User called ProxyUser that has the active_for_authentication? return true. Something like this:
class ProxyUser < User
# If you have a type column on User then uncomment this line below
# as you dont want to expect ProxyUser to have type 'ProxyUser'
#
# self.inheritance_column = :_type_disabled
devise :database_authenticatable
def active_for_authentication?
true
end
end
Then in the controller you want something like this:
proxy_user = ProxyUser.find(params[:user_id])
sign_in :proxy_user, proxy_user
Also in your routes you will need devise to expect ProxyUser so include:
devise_for :proxy_users
And finally when you sign this user out (assuming you can sign the user out in your controller code) make sure to tell devise the scope of the sign out, so you would do
sign_out :proxy_user
And then finally note that in your app you may be expecting current_user in different places (such as if you use CanCanCan for authorization) and now when you sign in as a proxy_user your app will return current_user as nil. Your app will instead have an object called current_proxy_user that will be your signed-in ProxyUser object. There are many ways to handle the issues resulting from your current_user returning nil in this case (including overwriting current_user in your application controller).

Disable Single user multi login rails with rails

I was trying to disable multi login for single user using devise authentication and came up with this solution.
I used active record session store, so that I can access all user session by their ID. Changed initializer session_store.rb to
MyApp::Application.config.session_store :active_record_store
added field session_id to user table and created model for session(session.rb)
rails migration:
rails g migration add_session_id_to_users session_id:integer
session.rb:
class Session < ActiveRecord::Base
end
Since, devise gem using warden I set callback after_authentication to check user session.
user.rb:
class << self
def delete_session(session_id)
begin
session = Session.where("session_id=?",session_id)
session.delete_all
rescue => e
end
end
end
Warden::Manager.after_authentication do |user, auth, opts|
unless user.session_id == ""
auth.logout
User.delete_session(user.session_id)
user.session_id = ""
user.save
throw(:warden, :message => "User already logged in, try again wil singout from other machine")
end
end
This callback will alert when user already logged in.
I was storing user session id in application_controller before_filter (aplication_helper.rb)
before_action :save_session
def save_session
if user_signed_in? && current_user.session_id == ""
current_user.session_id = request.session_options[:id]
current_user.save
end
end
I was getting session ID by request.session_options[:id].
This setup works fine without any problem. Guys can you please share your suggestion on this implementation. Any potential problem will occur by this solution?
There was a similar question,
Devise - Invalidate user session if the same user logs in from a different browser/machine
The basic idea is the same, differently implemented.
Personally like it since its all neatly in the controller rather than in the model.

How can I make Devise immediately inform user of expired password reset token?

If a user requests 2 password reset links in a row, then only the most recent one is valid.
If a user clicks on the older one, they are not immediately informed of the token being invalid. They are asked to type in the new password twice. Only upon hitting submit are they informed that the token is not valid.
Is there a standard way to change this behavior, so the user is immediately informed that they used the wrong link?
I was a bit surprised to find out that this is standard behavior - thanks for bringing it up! I would probably use a custom controller that inherits from Devise::PasswordsController and simply override :edit. The trouble is we're working with an abstract user (since no user is actually authenticated at this point), so we can't check it against a particular token in the database, and we have to check whether the given token exists.
class PasswordsController < Devise::PasswordsController
before_filter :validate_reset_password_token, only: :edit
private
def validate_reset_password_token
recoverable = resource_class.find_by_reset_password_token(params[:reset_password_token])
redirect_to root_path unless (recoverable && recoverable.reset_password_period_valid?)
end
end
This will redirect unless a user exists with the token passed. You can handle the redirection/error display however you wish.
Finally, in your routes file, change the controller used by devise for passwords:
devise_for :users, :controllers => { passwords: "passwords" }
Caveats:
I have not tested this (but I might look into it tomorrow).
There may be some additional security issues to consider - could someone realistically gain access to a random account? (probably not; Devise uses a similar method of locating a model instance using the reset token in reset_password_by_token)
By the way, there is no way to tell them that they used the wrong link and they should click the other one, as there is no way to positively identify them in the first place, and therefore no way to know that they have already generated another valid token.
There is another safety solution for this question;
class User::PasswordsController < Devise::PasswordsController
def update
super do |resource|
# Jump to super code
next unless resource.errors.any?
token_errors = resource.errors.details[:reset_password_token]
expired_error = token_errors.select { |detail| detail[:error] == :expired }
# Jump to super code
next unless expired_error.present?
message = resource.errors.full_messages_for(:reset_password_token).join(',')
return redirect_to new_user_password_path, alert: message
end
end
end

Rails and Authlogic: Allow only one session per user?

Is there a way to limit the number of sessions in Ruby on Rails application (I'm using Authlogic for authentication)?
I would like to allow only 1 session per user account. When the same user is logging on another computer the previous session should be expired/invalidated.
I was thinking about storing the session data in database and then deleting it when a new session instance is created but probably there is an easier way? (configuration option)
I just ran into a possible solution, if you reset presistence token you can achieve the intended behaviour:
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 logging in are invalidated.
Earlier I implemented it as you mentioned: add a session_key field to the users table and make sure that the current session_id is stored for the user on login:
class UserSession < Authlogic::Session::Base
after_save :set_session_key
def set_session_key
record.session_key = controller.session.session_id
end
end
Then in the generic controller do something like this to kick out a user when someone else logged in with that same account:
before_filter :check_for_simultaneous_login
def check_for_simultaneous_login
# Prevent simultaneous logins
if #current_user && #current_user.session_key != session[:session_id]
flash[:notice] = t('simultaneous_logins_detected')
current_user_session.destroy
redirect_to login_url
end
end
i do exactly what your talking about, assign a session id to each uniq session, store that id in a cookie and the associated session data in a table. Works well. My aim wasnt to limit users to a single session, but rather keep the session variables server side to prevent user manipulation.

authlogic UserSession.create(#user) giving unauthorized_record

I am trying to create a session explicitly like this UserSession.create(#user, true) but the session is not getting created, current_user is nil.
But when I do this, I get < #UserSession: {:unauthorized_record=>""}>us = UserSession.create(#user, true)
RAILS_DEFAULT_LOGGER.info(us.inspect) #=> UserSession: {:unauthorized_record=>""}
I had a look at Authlogic::Session::UnauthorizedRecord here it says
Be careful with this, because Authlogic is assuming that you have already confirmed that the user is who he says he is. For example, this is the method used to persist the session internally. Authlogic finds the user with the persistence token. At this point we know the user is who he says he is, so Authlogic just creates a session with the record. This is particularly useful for 3rd party authentication methods, such as OpenID. Let that method verify the identity, once it’s verified, pass the object and create a session.
which is exactly what I am trying to do (i am authenticating using omniauth and creating session using authlogic).
How do I fix this, so that I can get a valid session in current_user ?
I had a similar issue caused by the persistence_token being nil on the user. Reset it before creating the UserSession. So...
#user.reset_persistence_token!
UserSession.create(#user, true)
I'm not sure about the .create(object, bool) method signature, but the following works using authlogic.
class Api::ApiBaseController < ApplicationController
protected
def verify_token
return false if params[:token].blank?
#session = UserSession.new(User.find_by_single_access_token(params[:token]))
#session.save
end
end
If that doesn't work for you -- I think the #user isn't being set correctly.
If you map the active_record_store to the authlogic user_sessions table your session information will be stored in the database, and you will be able to store larger sets of data.
Inside your config folder:
config/initializers/session_store.rb
Comment out App::Application.config.session_store :cookie_store, :key => '_App_session'
Add or uncomment App::Application.config.session_store :active_record_store
Inside of config/application.rb
At the end of the class for you application add:
ActiveRecord::SessionStore::Session.table_name = 'user_sessions'
Restart your app, and any information stored in the user session will be saved in the authlogic user_sessions table.
Goto: http://apidock.com/rails/ActiveRecord/SessionStore
For more information
For now you can replace
UserSession.create #user
to
UserSession.create :email => #user.email, :password => #user.password
not a big deal.
But that caught me other way. I forgot that my user got active? == false when created. I've set it to true and session is created.
I ran into this problem today. In my case it ended up being related to CSRF tokens.
We are creating a user and session in our app in response to an OAuth callback. It appears that if the CSRF token is invalid, which would be the case when coming from a third party, authlogic won't create the user session.
Can't verify CSRF token authenticity
The fix was simple:
class Oauth::UserSessionsController < ApplicationController
skip_before_action :verify_authenticity_token, only: :callback
def new
# code removed...
end
def callback
# code removed...
UserSession.create(#user)
redirect_to root_path
end
end

Resources