find and terminate all active sessions by user_id - ruby-on-rails

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

Related

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.

Optimise query made by Devise to find user based on remember_token

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

Generate unique usernames in rails

How can I generate unique usernames based on a format. So for example the first user signs up. The username for him is AAA001 the second user signs up the username will be AAA002, and it keeps incrementing like that. How can I set it up so even if two users click sign up at the same time the database just sends them each a unique username. This is gonna be done on RoR.
Thank you in advance
you can generate the username in the after_safe hook of the model and save! again in a transaction (to prevent race conditions) and use the DB-ID of the already saved user to generate the username
You need to use database transactions.
Here is an example:
ActiveRecord::Base.transaction do
#code goes here
end
This will ensure that the code inside the transaction is blocking.

How can I find a devise user by it's session id?

Given session["session_id"] is it possible to find the logged in User to which that session belongs to?
At least on my system (rails 3.2, devise 2.0.4), you can do it like this:
session is:
{"session_id"=>"be02f27d504672bab3408a0ccf5c1db5", "_csrf_token"=>"DKaCNX3/DMloaCHbVSNq33NJjYIg51X0z/p2T1VRzfY=", "warden.user.user.key"=>["User", [3], "$2a$10$5HFWNuz5p6fT3Z4ZvJfQq."]}
session["warden.user.user.key"][1][0], then is 3.
So, I'd find it as:
User.find(session["warden.user.user.key"][1][0])
I'm not sure what you are trying to accomplish but will try to answer
If you want only the User from the current session, a simple way would be to store his id on session, for example:
def login(username, pass)
# do the usual authentication stuff and get user
if logedin
session[:user_id] = user.id
end
end
Then get the current user would be something like
current_user =User.find(session[:user_id])
If what you want is finding all the users that are currently logged in, I think you need to config your app to save session at DB, because predefined is that session data is store in cookies in the browser. For more information on this check this answer
How to track online users in Rails?
EDIT: Just noticed you are using devise so the first way is actually there for you. You just need to call current_user method at any controller.
For the second way check this answer "Who's Online" using Devise in Rails
And i might add this, as i was trying to do it the other way, if you are using ActiveRecord session_store you can find all stored sessions of a user like so:
ActiveRecord::SessionStore::Session.select{ |s| s.data["warden.user.user.key"] && s.data["warden.user.user.key"][0][0] == user_id }

How to store where a new user was referred? Using Rails + Devise

I have a rails app that uses devise. I'm curious to know, is it possible in the User table to somehow track where a new user came from, the HTTP referrer?
I'd like to know which came from Facebook, Twitter, LinkedIn, Google+ in order to track a viral loop.
Any ideas? Seen anyone do this? Possible? Where should this live in the rails app? Still very new. Thanks
It could be done like this. May require some tweaking and fixing but You'll get an idea
Make before filter for Application controller, you will call it for any action
def landing_filter
if from_other_site(request.referrer) and !session[:referer].blank?
session[:referer] = request.referrer #you don't want to delete first entrance
end
end
from_other_site should be the method which will check domain name in referrer url, if it match your then return false, otherwise true
in devise/registration/new.erb.html view add in form hidden field
<%= f.hidden_field :referrer, session[:referrer] %>
and don't forget to add migration with new database field for user
Save referer somewhere and after creating a user copy information to user table. Using session to save referer works but permanent cookies are better. Cookies can persist the information even when user closes browser and comes again in the next day.
# so basically in ApplicationContreller using before_filter
def referer_before_filter
if cookies[:referer].blank?
cookies.permanent[:referer] = request.env["HTTP_REFERER"] || 'none'
end
end
# and in signup action somewhere else saving that information
#user.referer = cookies[:referer] # or maybe to some other table
Instead of modifying every action you can also use rails sweepers/observers to handle automatic saving every time an object is created.
A good gem to automatically save referer and other needed information is
https://github.com/holli/referer_tracking . You can choose do you want to save information manually or use sweepers to do saving automatically.

Resources