Our Rails app is using Restful Authentication for user/session management and it seems that logging in to the same account from multiple computers kills the session on the other computers, thus killing the "Remember me" feature.
So say I'm at home and log in to the app (and check "Remember me"). Then I go to the office and log in (and also check "Remember me"). Then, when I return home, I return to the app and and have to re-log in.
How can I allow logging in from multiple machines and keep the "Remember me" functionality working across them all?
You are going to sacrifice some security by doing this, but it's definitely possible. There are two ways you should be able to accomplish this.
In the first, you can override the make_token method in your user model. The model is currently implemented as follows.
def make_token
secure_digest(Time.now, (1..10).map{ rand.to_s })
end
Every time a user logs in, with or without a cookie, the make_token method is called which generates and saves a new remember_token for the user. If you had some other value that was unique to the user that couldn't be guessed, you could replace the make_token method.
def make_token
secure_digest(self.some_secret_constant_value)
end
This would ensure that the token never changes, but it would also enable anyone that got the token to impersonate the user.
Other than this, if you take a look at the handle_remember_cookie! method in the authenticated_system.rb file, you should be able to change this method to work for you.
def handle_remember_cookie!(new_cookie_flag)
return unless #current_<%= file_name %>
case
when valid_remember_cookie? then #current_<%= file_name %>.refresh_token # keeping same expiry date
when new_cookie_flag then #current_<%= file_name %>.remember_me
else #current_<%= file_name %>.forget_me
end
send_remember_cookie!
end
You'll notice that this method calls three methods in the user model, refresh_token, remember_me, and forget_me.
def remember_me
remember_me_for 2.weeks
end
def remember_me_for(time)
remember_me_until time.from_now.utc
end
def remember_me_until(time)
self.remember_token_expires_at = time
self.remember_token = self.class.make_token
save(false)
end
#
# Deletes the server-side record of the authentication token. The
# client-side (browser cookie) and server-side (this remember_token) must
# always be deleted together.
#
def forget_me
self.remember_token_expires_at = nil
self.remember_token = nil
save(false)
end
# refresh token (keeping same expires_at) if it exists
def refresh_token
if remember_token?
self.remember_token = self.class.make_token
save(false)
end
end
All three of these methods reset the token. forget_me sets it to nil, whereas the other two set it to the value returned by make_token. You can override these methods in the user model, to prevent them from resetting the token if it exists and isn't expired. That is probably the best approach, or you could add some additional logic to the handle_remember_cookie! method, though that would likely be more work.
If I were you, I would override remember_me_until, forget_me, and refresh_token in the user model. The following should work.
def remember_me_until(time)
if remember_token?
# a token already exists and isn't expired, so don't bother resetting it
true
else
self.remember_token_expires_at = time
self.remember_token = self.class.make_token
save(false)
end
end
#
# Deletes the server-side record of the authentication token. The
# client-side (browser cookie) and server-side (this remember_token) must
# always be deleted together.
#
def forget_me
# another computer may be using the token, so don't throw it out
true
end
# refresh token (keeping same expires_at) if it exists
def refresh_token
if remember_token?
# don't change the token, so there is nothing to save
true
end
end
Note that by doing this, you're taking out the features that protect you from token stealing. But that's a cost benefit decision you can make.
You can change what the remember_token is to achieve this. You can set it to:
self.remember_token = encrypt("#{email}--extrajunkcharsforencryption")
instead of
self.remember_token = encrypt("#{email}--#{remember_token_expires_at}")
Now there is nothing computer or time specific about the token and you can stay logged in from multiple machines.
Related
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
For some reason after some time on my website my session hash is turning into a string
undefined method `admin?' for "#<Visitor:0x000001071b7800>":String
is what I'm getting in my render_layout method
def render_layout
if session[:visitor].admin?
render layout: 'admin'
else
render layout: 'application'
end
end
the only two other times I ever call or use session[:visitor] is in my authenticate method, and my logged_in? method that i use to skip authenticate
def authenticate
uuid = params[:uuid]
#visitor ||= uuid && Visitor.find_by_uuid(uuid)
if !#visitor
authenticate_or_request_with_http_basic do |login, password|
#visitor = Visitor.find_by_uuid(ENV['ADMIN_UUID']) if login == 'test' && password == 'testpw'
end
session[:visitor] = #visitor
else
session[:visitor] = #visitor
end
end
def logged_in?
!!session[:visitor]
end
Why is this getting turned into a string? I used a project search in atom and I only ever called it in those places.
Edit:
I've added a binding.pry at the 4 locations I call session[:visitor] and it works the first time through everything. As soon as I follow a url for the first time and
before_action :authenticate, unless: :logged_in?
gets called for a second time the session[:visitor] is turned into a string
#=> "#<Visitor:0x00000106851bd0>"
From the docs, http://guides.rubyonrails.org/security.html#sessions
Do not store large objects in a session. Instead you should store them
in the database and save their id in the session. This will eliminate
synchronization headaches and it won't fill up your session storage
space (depending on what session storage you chose, see below). This
will also be a good idea, if you modify the structure of an object and
old versions of it are still in some user's cookies. With server-side
session storages you can clear out the sessions, but with client-side
storages, this is hard to mitigate.
Store your visitor's ID in the session
session[:visitor_id] = #visitor.id
and then retrieve it as needed
#visitor = User.find_by_id(session[:visitor_id])
I have this in my :show action of users_controller.
def show
#user = User.find(params[:id])
end
But there are some columns in the users table that I wouldn't want accessed from the #user instance variable.
There is the encrypted_password column and the salt column. What can i do on the model or the controller to ensure that #user has no password or salt values.
I want when I do #user.password or #user.salt, it returns nil or something that can't compromise a user's security.
Limiting your Ruby code from fetching some data from the DB hardly enhances security - chances are a hacker will get to your DB not through your ruby code, but by hacking straight to you database...
If all that is saved in the database is an encrypted password and the salt (both of which you need to authenticate the user) - you should be fine, having both is not enough to know the user's password (at least not easily, assuming the encryption is strong enough)
If you want to be extra careful, you can save the salt in a separate repository than the encrypted password repository. This way a hacker will have to break into both repositories to even start brute forcing your users' passwords.
Don't store the password in the database in unencrypted format. Ever.
Hiding an attribute in an ActiveRecord model provides some basic information on how you could hide various attributes in the model class.
The best approach would be to serve a facade to your controller from some intermediate object, and the facace would basically only expose those fields that the controler needed to manipulate.
You can add an after_find callback in the model...
class User << ActiveRecord::Base
after_find :nil_secure_fields
def nil_secure_fields
password = nil
salt = nil
end
end
If the record is updated you'd want to ensure the password and salt attributes aren't included in the attributes to update.
For User model add
class << self
def find_secure(id)
user = self.find(id)
user.secure_attributes!
user
end
end
def secure_attributes!
#attributes_secure = true
end
def secure_attributes?
!!#attributes_secure
end
def encrypted_password
secure_attributes? ? nil : super
end
def salt
secure_attributes? ? nil : super
end
Now you can use find_secure method instead of find which will restrict access to secure attributes.
PS This is not obviate the need to store passwords encrypted.
In my rails 3.1 application, I want to create and expire random password for users.I am using devise gem for that.Any plugin available for expiring password withing some duration?
Or else Please give me some logical advice to implement this feature.
Please consider me as a newbie.
It sounds like you just want to expire the password once. If you're looking to do it at regular intervals (e.g. every couple of months) or if you want to prevent users from re-using passwords, it gets more complicated.
Taken from an app I'm working on:
app/models/user.rb (assuming that's what you name your model):
def password_should_expire?
# your logic goes here, remember it should return false after the password has been reset
end
app/controllers/application_controller.rb
before_filter :check_password_expiry
def check_password_expiry
return if !current_user || ["sessions","passwords"].include?(controller_name)
# do nothing if not logged in, or viewing an account-related page
# otherwise you might lock them out completely without being able to change their password
if current_user.password_should_expire?
#expiring_user = current_user # save him for later
#expiring_user.generate_reset_password_token! # this is a devise method
sign_out(current_user) # log them out and force them to use the reset token to create a new password
redirect_to edit_password_url(#expiring_user, :reset_password_token => #expiring_user.reset_password_token, :forced_reset => true)
end
end
When you create a password, note the time it was created. Then, when the password is being used, check that the password was created less than 24 hours ago.
Depending on what frameworks you are using, this functionality (or something similar) may already exist within the framework, or perhaps as a plugin. If not, it isn't particularly difficult to implement. All you would need is an extra column in your data store to hold the password creation date/time, and a bit of extra logic on password creation and on password use.
Check out the Devise Security Extension gem:
https://github.com/phatworx/devise_security_extension
I've been using it for expiring passwords and archiving passwords (to make sure an old password is not reused) with no problems.
#Jeriko's answer contains some old code, here are the edits
In model/user.rb:
def password_should_expire?
if DateTime.now() > password_changed_at + 30.seconds
return true;
else
return false;
end
end
In Application Controller:
before_filter :check_password_expiry
def check_password_expiry
return if !current_user || ["sessions","passwords"].include?(controller_name)
# do nothing if not logged in, or viewing an account-related page
# otherwise you might lock them out completely without being able to change their password
if current_user.password_should_expire?
#expiring_user = current_user # save him for later
#expiring_user.set_reset_password_token! # this is a devise method
sign_out(current_user) # log them out and force them to use the reset token to create a new password
redirect_to edit_password_url(#expiring_user, :reset_password_token => #expiring_user.reset_password_token, :forced_reset => true)
end
end
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.