Limit sessions number concurrent per account user devise, rails 4 - ruby-on-rails

Using Rails 4.0 and ruby 2.1.0
I'm working on a project that will be subscription based and the accounts need to have a limit of concurrent sessions per user. For example, normal user 1 concurrent session per account, medium user - 3 concurrent sessions per account and premium user 10 concurrent sessions per account.
I have checked every page of google, stackoverflow, differents gems like devise:timeoutable or devise_session_limit gem. I have tried with a field called connections which is 1, 3 or 10 depending on user. If user login, decrement -1 the connections field, if user logout then increment +1 connectios field. The thing or problem is when the user keep open the session and leave, then never will increment +1.
Any suggestions?? I am working on this task almost 3 days and I cannot get it.
Thanks in advance!

You can hook into devise and as long as you have the :timeoutable module you can check that a user has been timed out and run whatever code you like. See this warden hook for reference: https://github.com/plataformatec/devise/blob/master/lib/devise/hooks/timeoutable.rb
The important part of that code is the check for record.timedout?. If that's true, run your connections increment code.
To be specific, write a config/initializers/connections_updater.rb file with this (similar to above linked code it is a warden hook):
Warden::Manager.after_set_user do |record, warden, options|
scope = options[:scope]
env = warden.request.env
if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) && options[:store] != false
last_request_at = warden.session(scope)['last_request_at']
if last_request_at.is_a? Integer
last_request_at = Time.at(last_request_at).utc
elsif last_request_at.is_a? String
last_request_at = Time.parse(last_request_at)
end
proxy = Devise::Hooks::Proxy.new(warden)
if record.timedout?(last_request_at) && !env['devise.skip_timeout']
# write this method to do what you need when the session times out
record.increment_connections_count! # Note: record is the user model
Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
end
end
end

Related

Authlogic gem: use last_request_at column at session level not in user level

Problem: If I logged in as the same user on two devices(say A and B) and I use my application in one device(A) whereas the other device(B) remains inactive. The device B does not logout when the session expires while using feature logout_on_timeout.
I am trying to implement logout_on_timeout feature of authlogic gem, which I successfully implemented but the problem is authlogic updates the last_request_at attribute of User in every request no matter the browser or devices where it logged in. So if I logged in the same user from mobile as well as from desktop and one of the device is active then the other device remains active too because it uses the same shared last_request_at attribute from User.
Reference code from authlogic gem: lib/authlogic/acts_as_authentic/logged_in_status.rb
# Returns true if the last_request_at > logged_in_timeout.
def logged_in?
unless respond_to?(:last_request_at)
raise(
"Can not determine the records login state because " \
"there is no last_request_at column"
)
end
!last_request_at.nil? && last_request_at > logged_in_timeout.seconds.ago
end
So How can I solve this problem? Is there any way to implement it at the session-level? Like using last_request_at in UserSession model.
I decided to not use the logout_on_timeout feature but rather use the remember_me feature then override the remember_me_for method in UserSession and set the dynamic value. Because logout_on_timeout uses last_request_at attribute value from the User to check if session timeout or not, and last_request_at is shared in all session, so When a user opens an app in two devices then both will remain active if one of them is active(kinda incomplete feature or bug maybe).
Also, update the session(ActiveRecord::SessionStore::Session) for the current user and change the session's data column value with the generated value from instance method generate_cookie_for_saving of UserSession at the end of each request because authlogic only update the session only when creating or deleting the UserSession.
Note: I am using activerecord-session_store gem to persist session in the database.

find and terminate all active sessions by user_id

I'm using cookie based sessions on a Rails 6 app with the following setup:
Rails.application.config.action_dispatch.cookies_serializer = :marshal
Rails.application.config.session_store :cookie_store, expire_after: 14.days
After a user changes his password I'd like to terminate all his active sessions, which could be in other browsers and/or devices. Something like this:
ActionDispatch::Session::CookieStore.where(user_id: session[:user_id]).destroy_all
Only those aren't really valid methods for CookieStore. Any idea how to accomplish this?
Unfortunately, you cannot accomplish this via sessions no matter the way of session storage.
To be able to do this, you have to keep the list of active logins on the server in separate DB table and delete those records instead eg:
You create new table eg logins with columns user_id and token. Every time user logs in, you will create new record in this table and then you save user_id and token to the session:
def login
# ...
# user authorization code
# ...
login = user.logins.create(token: SecureRandom.uuid)
session[:user_id] = login.user_id
session[:user_token] = login.token
end
and every time you are loading user from the session you have to do two steps:
find user by ID
check user_token validity
def authorized_user
#authorized_user ||= begin
login = Login.find_by(user_id: session[:user_id], token: session[:user_token])
return if login.blank?
login.user
end
end
And now, every time you want to logout user you just have to remove corresponding record from logins table.
In your case you want to logout user from all other devices, only thing you need to do is to execute following code:
authorized_user.logins.where.not(token: session[:user_token]).delete_all
and you are done.
Of course this is just a simple example, you also can hash or encrypt tokens, so they are not directly readable or you can add expiration date and automatically log out users when the date is exceeded, etc..

Rails/Devise - how to logout a user after an `x` minutes of inactivity?

I am developing a system which contains many roles and one of the roles is ADMIN which can access critical part of the system, I want to limit the user's session which when he/she don't interact with the system for certain period of time, its session gets expires and the next time he/she should log in again, How can I do this?
Devise has a timeoutable module that you can use.
In your User model you would include :timeoutable with your devise models and in the devise.rb initializer you would configure it comparable to:
# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
config.timeout_in = ENV['YOUR_TIME'].to_i.minutes
If you want to be more flexible with your user types you can add a method something like this for your User model:
def timeout_in
if self.type == 'Admin'
45.minutes
else
60.minutes
end
end
That would be used instead of setting timeout_in in your initializer.
Edit: My first response was similar to the first answer in the thread below, but the second answer in that thread might be a better fit. Since it works directly with the session_store.
Here's a useful StackOverflow link that can provide extra info: Rails 4: Session Expiry?
This documentation has some info on the methods available for session manipulation: http://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html

Locking a user using a per-user `maximum_attempts` value with Devise for Rails

As a de-facto standard, we all using Devise for login in our Rails application and will use the Lockable module to lock users after a particular number of failed attempts.
From Devise’s source code and the configuration option config.maximum_attempts = 20, I came to understand how Devise performs locking when the user tries to give wrong login credentials. Configuration is statically defined at Rails application boot time in initializers.
My expectation is to set the maximum_attempts dynamically – is this possible? If so, please guide me.
I have a superadmin and user below each admin. Based on the super admin I would like to set a different failed_attempt value for each user during runtime.
One possible way is to monkey-patch the Devise code that you linked to, where attempts_exceeded? is defined. Here’s a guess at what needs to be overridden:
module Devise::Models::Lockable
# assumes that the User model has a `superadmin` relation
# that has a `maximum_attempts` attribute
def attempts_exceeded?
self.failed_attempts >= self.superadmin.maximum_attempts
end
def last_attempt?
self.failed_attempts == self.superadmin.maximum_attempts - 1
end
end
This should work, but it would mean that whenever you update Devise, there is a risk of related code breaking, with unknown consequences. So you would have to review the changes to Devise before every update. And if you are discouraged from updating Devise because of this, that may eventually cause security problems if you are too slow to update to a version of Devise with a fixed security problem. So beware of those possible problems.
A safer way that requires more work up-front is to lock the user manually from your own code. The documentation for Devise::Models::Lockable mentions a public method lock_access! that locks the user when you call it. You can set the global config.maximum_attempts to some really high value such as 25. Then, in some callback on the model (I’m not sure which callback), call a method lock_access_based_on_superadmin_limit! that calls lock_access! if necessary according to your custom rules. The following definition is adapted from part of Devise’s valid_for_authentication?:
class User
# …
def lock_access_based_on_superadmin_limit!
if failed_attempts >= superadmin.maximum_attempts
lock_access! unless access_locked?
end
end
end

Devise Remember Me and Sessions

I'm confused with the devise gem config settings:
# The time the user will be remembered without asking for credentials again.
config.remember_for = 2.weeks
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again.
config.timeout_in = 10.minutes
I want to have a user select the "Remember Me" checkbox (i.e., keep me logged in), but the default session timeout is 10 minutes. After 10 minutes it asks me to log in again even though I have clicked "Remember me". If this is true then the remember_for is really meaningless. Obviously I'm missing something here.
Ryan is correct in that the default Devise gem does not support both the :rememberable and :timeoutable options. However, like all things Ruby, if you don't like the decision that some other coder has made, especially when it strays from the norm that most users are likely to expect, then you can simply override it.
Thanks to a (rejected) pull request we can override this behaviour by adding the following code to the top of your Devise config file (/config/initializers/devise.rb):
module Devise
module Models
module Timeoutable
# Checks whether the user session has expired based on configured time.
def timedout?(last_access)
return false if remember_exists_and_not_expired?
last_access && last_access <= self.class.timeout_in.ago
end
private
def remember_exists_and_not_expired?
return false unless respond_to?(:remember_expired?)
remember_created_at && !remember_expired?
end
end
end
end
This will now allow you to configure both options and have them work as you would expect.
config.remember_for = 2.weeks
config.timeout_in = 30.minutes
The timeout_in will automatically log you out within 10 minutes of inactivity and is incompatible with the remember_me checkbox. You can have one, but not both.
The information in previous answers is outdated. I've tested my project, which uses Rails 4 and Devise 3.5.1 and also checked devise code to be sure.
Now it looks whether Remember Me checkbox was checked:
if yes, it checks if remember_exists_and_not_expired, so basically uses config.remember_for for session management
if no, it checks if last_access <= timeout_in.ago, using config.timeout_in correspondingly

Resources