Devise Remember Me and Sessions - ruby-on-rails

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

Related

Devise User's password gets overwritten upon updating any other attributes

Problem
When I use user.update(not_password_attribute: 'value') for the first time in the session the SQL query includes this:
UPDATE "users" SET "encrypted_password" = $1, so the encrypted password gets overwritten, and the password the user had before is no longer valid (checked with #valid_password? method).
However, when I update the same user for the second time within one session the encrypted_password is no longer added to the SQL query.
But when close the rails console and open it again the first scenario happens again.
Here is the output from the rails console: https://i.stack.imgur.com/ktTgJ.jpg
The same happens if I use user.update_without_password
here is the output
I used the console to demo that the problem is not within controllers but it happens whenever the user is updated from any of the controllers including Active Admin or UsersController etc.
I tried using Devise 4.7.2 which I used in a previous project where this issue did not occur. But the outcome was the same.
Question
How do I update the user without the password being overwritten?
Some explanation why this happens would also help.
I am using
Rails 6.0.3.4
Devise 4.7.3
Active Admin 2.9.0 b9076eb (don't think the issue is in Active Admin but trying to provide all the information here)
Devise Config (in /app/config/inintializers/devise.rb)
config.mailer_sender = 'please-change-me-at-config-initializers-devise#example.com'
require 'devise/orm/active_record'
config.case_insensitive_keys = [:email]
config.strip_whitespace_keys = [:email]
config.skip_session_storage = [:http_auth]
config.stretches = Rails.env.test? ? 1 : 12
config.reconfirmable = true
config.expire_all_remember_me_on_sign_out = true
config.password_length = 6..128
config.email_regexp = /\A[^#\s]+#[^#\s]+\z/
config.reset_password_within = 6.hours
config.sign_out_via = :delete
None of these seem relevant to the problem but, again, trying to provide as much information as possible
User model (in /app/models/user.rb)
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
# some methods here but none overwrites Devise or ActiveRecord methods
end
I cannot say I am completely sure what might be going there but I have a few guesses.
So, it seems like your issue is: user.update(first_name: 'one') is triggering a callback that attempts to update encrypted password even when an actual password is not provided or it is considered blank. But there is more.
Why it doesn't get triggered for the subsequent update (user.update(first_name: 'two')?
Possible explanation for that one is - perhaps callback is not triggered if logic behind it figures out that the password is now the same as the one we have set with the first command (ie blank) or the user object we call the second update on has encrypted password field populated after we executed the first update.
Ok, but then, rerunning the console again, and for the same user that we previously set blank password for, we again get encrypted_password update?
Looking at the encrypted password being set in the new console and observing that it is different than the previous one, we can reason that either blank case is handled by assigning random encrypted password, or there is a monkey-patch causing DeviseEncryptor (see https://github.com/heartcombo/devise/blob/98fc5e8e396b66b826528811287ea6680a6d0757/lib/devise/models/database_authenticatable.rb#L71) to somehow generate different result based on current session (this would probably break a lot more things thus highly-unlikely).
The weirdest one is update_without_password still adding encrypted password to query???
Explanation - whatever was previously mentioned seems to affect update_without_password in a similar manner.
The solution
I think you can solve your issue if you use update methods that skip callbacks like: update_column or update_columns (see: https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks)
If you really want to understand what is going on exactly I would suggest to take a look at this file from devise source: https://github.com/heartcombo/devise/blob/master/lib/devise/models/database_authenticatable.rb. Then, after you get a grasp of the code there, you can perhaps find the same source file in your local environment (wherever devise gem is installed), and instrument it with logs (puts) and check / patch it out yourself.
Note: a few nice 'techniques' are described here:
https://tenderlovemaking.com/2016/02/05/i-am-a-puts-debuggerer.html
It would also be a good idea to disable spring while debugging weirdnesses like this. Good luck!
As a bonus - you can get some more context about why things around devise user update work the way they do by reading this discussion: https://github.com/heartcombo/devise/issues/5033 I believe ActiveAdmin was also mentioned in the discussion. Tbh, I originally thought you had the same issue as them but the affected version does not match with the one you shared.

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

Limit sessions number concurrent per account user devise, rails 4

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

set timeout for a token in rails

How do i set a timeout for a given authentication token? After the timeout, the token will be deleted and the user won't be able to use it on his requests.
I'm using rails 3 and devise.
I was looking for this feature too, but didn't find a way to do it directly.
You can reset the authentication token on each sign-in and use the rememberable in-between:
in your application controller, in after_sign_in_path_for():
resource.reset_authentication_token
in devise.rb:
config.remember_for = 1.day
Or you can create a cron-job to periodically clear the invalid authentication_token entries from the users table.
I'm not sure if that's exactly what you are looking for, but this is a simple option in Devise.
If you set the following option in config/initializers/devise.rb
config.timeout_in = 30.minutes
then Devise will expire the token after 30 minutes of inactivity. The same operations that Devise does for session authentication should also work with the authentication_token.
I have used that in my current project and tested it using Timecop gem:
it "should timeout without activity after 30 minutes" do
auth_token = #user.authentication_token
get "/frontend/users/#{#user.id}.json?auth_token=#{auth_token}"
response.status.should == 200
Timecop.travel(45.minutes.from_now)
get "/frontend/users/#{#user.id}.json?auth_token=#{auth_token}"
response.status.should == 401
Timecop.return
end
Also, I don't believe that the token follows the same analogy as user/password combination as mentioned in one of the comments, since you wouldn't store your password in plain text but you do with your token. I would recommend resetting the token after each logout as well.
At devise initializer file
#/config/initializers/devise.rb
# ==> 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 = 1.day
# If true, expires auth token on session timeout.
config.expire_auth_token_on_timeout = true

Resources