Optimise query made by Devise to find user based on remember_token - ruby-on-rails

I am using Devise-1.5.4 with Rails 3.0.20.
Devise provides methods like current_user, authenticate_user! which call authenticate!, which itself calls serialize_from_cookie, that uses remember_token to authenticate the user.
Also, the serialize_from_cookie method receives id as a parameter, so that it queries Users table on the primary key (which is automatically an optimised query).
However, I see queries like select * from users where remember_token = 'XXXXXX' in MySQL logs.
Since the users table has grown huge, these queries are getting slower. I have following questions regarding this:
I am not able to debug where (in code) is Devise making such queries?
How can I optimise these queries (apart from adding indexes)?

Devise does a query to ensure a same token is not already set while setting a new remember_token.
https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L146
# Generate a token checking if one does not already exist in the database.
def remember_token #:nodoc:
loop do
token = Devise.friendly_token
break token unless to_adapter.find_first({ remember_token: token })
end
end

Related

How to select attributes when devise setting current_user?

At this point, I'm not sure how or where the helper current_user is loaded. I'd like to select only certain properties from the user table, but devise to
select * from users table
I want to do something like select (id, email, additional_stuff) from users
I would like to be able to modify the current_user set by devise so I can optimise my application from a security point.
Using Rails version 7.0.4
RUby Version 3.1.3
Devise is build on top of the Warden gem which handles the grunt work of actually authenticating users.
Fetching the user from the database is done by the Warden::Manager.serialize_from_session method which can be reconfigured.
# app/initializers/devise.rb
config.warden do |manager|
manager.serialize_from_session(:users) do |id|
User.select(:id, :email, :additional_stuff)
.find(id)
end
end
However, I'm very sceptical that this will have any real benefits to security and you'll most likely just end up breaking parts of Devise/the rest of your application. For example authenticating the user for password updates may fail unless you load the password digest.
Make sure you have tests covering your whole Devise implementation before you monkey around with it.
It's hard to know what you actually mean by "Certain class in the application use the current_user object, one such class is template creator" . But that smells like a huge gaping security hole in itself. If this is running code that comes from the user it should not have access to the entire view context (for example the current_user method). If the user has access to the actual object then whats preventing them from querying and getting any missing data?
If you really need this feature you should be sandboxing it in a separate renderer (not using Rails build in render methods) which only has access to the context and data you explicitly pass (like a struct or decorator representing a user) which is deemed safe.

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

Can someone explain the def current_user method in rails pls

This is the line I don't quite get.
#_current_user ||= session[:current_user_id] && User.find(session[:current_user_id])
If there is a User model with an email field and a password field what is User.find looking for? ie there is no session field stored in the User model.
Session has to do with the controller in your application.
A session is a way to store information (in variables) to be used across multiple pages. Unlike a cookie, the information is not stored on the users computer.
Sessions are usually saved as a key: value hash pair, and they can get expired.
so, according to the code example you gave:
#_current_user ||= session[:current_user_id]
User.find(session[:current_user_id])
The line: #_current_user ||= session[:current_user_id] is setting the #_current_user to the value of current_user_id in session, if #_current_user is nil.
User.find(session[:current_user_id]) on the other hand is getting the value of current_user_id in session, which should be an id, and is going to the database to find the user with that id.
So, User.find(session[:current_user_id]) is finding by id, and not by email or by password
#current_user is meant as an instance variable to bring back the User object for the currently logged-in user.
The typical use case for #current_user (at least to Devise users like us) is to utilize it within our code infrastructure:
def create
#model = current_user.models.new ...
end
Thus, the answer to your question:
what is User.find looking for?
... is that it's looking for the User object for the signed-in member.
I think you're getting confused with how an authentication system would be expected to work. Namely that once you log in (authenticate), the app sets a session (as described by Sunny K) to denote which user is browsing.
This is why you have User.find(session[:current_user_id]) -- your authentication system (whether homebrew or pre-packed) has already validated the email & password. Now it has to keep track of which user you are, so that each time you send a request, it can rebuild your current_user object.
--
Another important factor is the fact that HTTP is "stateless" - meaning that each request has to be "new" (IE the state has to be recreated each time).
This is opposed to a stateful application, such as a game, where the fact you're running the application allows you to retain its state throughout the session.
As such, when Rails receives requests from the browser, it does not "remember" who you are - it's literally a dumb machine.
If you want access to user-specific information, there needs to be a way to firstly authenticate the user (which you have), and then authorize their request. The authorization part is up to you, but in order to make it work, you basically have to send the current user id to the app each time.
Then, when you invoke an instance of your program, the #current_user object will be available. This is what User.find(session[:current_user_id]) is for.

How can I allow Devise users to log in when they're outside my default scope?

I have a Rails 4 app which uses Devise 3.4 for authentication, which I've customized with the ability to ban users (using a simple boolean column users.banned, default false). The User model also has a default_scope which only returns non-banned users.
Here's the problem - I still want my banned users to be able to log in, even though they can't do anything after logging in. (They essentially just see a page saying "you've been banned"). But it seems that the default_scope is tripping up Devise. When you log in or call e.g. authenticate_user!, Devise tries to find the current user using one of the basic ActiveRecord methods like find or find_by, but can't because they lie outside the default scope. Thus Devise concludes that the user doesn't exist, and the login fails.
How can I make Devise ignore the default scope?
After a long time digging around in the Devise and Warden source code, I finally found a solution.
Short Answer:
Add this to the User class:
def self.serialize_from_session(key, salt)
record = to_adapter.klass.unscoped.find(key[0])
record if record && record.authenticatable_salt == salt
end
(Note that I've only tested this for ActiveRecord; if you're using a different ORM adapter you probably need to change the first line of the method... but then I'm not sure if other ORM adapters even have the concept of a "default so
Long Answer:
serialize_from_session is mixed into the User class from -Devise::Models::Authenticatable::ClassMethods. Honestly, I'm not sure what it's actually supposed to do, but it's a public method and documented (very sparsely) in the Devise API, so I don't think there's much chance of it being removed from Devise without warning.
Here's the original source code as of Devise 3.4.1:
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
The problem lies with to_adapter.get(key). to_adapter returns an instance of OrmAdapter::ActiveRecord wrapped around the User class, and to_adapter.get is essentially the same as calling User.find. (Devise uses the orm_adapter gem to keep it flexible; the above method will work without modification whether you're using ActiveRecord, Mongoid or any other OrmAdapter-compatible ORM.)
But, of course, User.find only searches within the default_scope, which is why it can't find my banned users. Calling to_adapter.klass returns the User class directly, and from then I can call unscoped.find to search all my users and make the banned ones visible to Devise. So the working line is:
record = to_adapter.klass.unscoped.find(key[0])
Note that I'm passing key[0] instead of key, because key is an Array (in this case with one element) and passing an Array to find will return an Array, which isn't what we want.
Also note that calling klass within the real Devise source code would be a bad idea, as it means you lose the advantages of OrmAdapter. But within your own app, where you know with certainty which ORM you're using (something Devise doesn't know), it's safe to be specific.

Resources