Preserving Rails 4 Devise Session Id Before & After Sign In - ruby-on-rails

I'm trying to implement an e-commerce app. In it, I allow the user to browse for products and put them into the cart even before signing in. I only prompt the user to sign in upon checkout.
However, I'm losing track of the user because the user's session id changes upon sign in. Due to this, Im unable to associate the items that the user placed into the cart (stored in redis) to the user who placed them in after the user signs in to the application.
Does anyone have any idea how could this be circumvented?
Thanks.
Cheers!

Found the solution. All that needs to be done is set session.options[:renew] = false and the session id will still be the same before & after signing in.
Please refer to the implementation below
class SessionsController < Devise::SessionsController
respond_to :json
def create
super
session.options[:renew] = false
end
def destroy
logger.info "Logging out: #{current_user.email}; Session Id: #{session.id}"
$redis.del "cart_#{session.id}"
super
end
end

Related

Limiting number of simultaneous logins (sessions) with Rails and Devise? [duplicate]

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

Need help: Automatically refresh a page (rails)

I have a test application that has a subscription. The subscription expires and I have a controller that handles the renew subscription(I know it's not good practice. I hired a cheap freelancer to do it and I don't really know how to program). The only way I know how to trigger a controller is with a link. I have a variable called 'days_til_expire' that will count down to zero.
Main Question: Is there a way to automatically trigger a refresh that will activate the controller I need. I'm thinking:
if days_till_expire == 0
page refresh to controller#renew
end
As an alternative to Wayne's answer:
Whenever user performs a "restricted" action (click to watch a video or whatever it is that your app hides behind the paywall), check subscription status and redirect to renew page if it's expired.
No need to bother with page refreshes. Unless, of course, your app has rich client-side UI (meaning, lots of javascript).
You don't need to trigger the link, you can use polling to check if expired or not, if expired, use js to redirect user.
To further #Sergio Tulentsev's answer, which is further to #Wayne Chu's answer (answer-ception?????), this is something called authorization, and can be handled entirely in the backend.
Simple implementation:
#app/models/user.rb
class User < ActiveRecord::Base
def expired?
# logic for whether subscription has expired
end
end
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authorized?
private
def authorized
if current_user.expired?
redirect_to root_path, notice: "Your Subscription has Expired"
end
end
end
This will fire every single time your user browses to a controller which inherits from the ApplicationController (BlogsController < ApplicationController).
--
A more integrated approach (instead of having to call a method every single time you browse to a controller) would be to validate against specific objects (using Pundit):
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
#user = user
#post = post
end
def update?
user.subscribed?
end
end
The above would be accompanied by a daily process which would cycle through your users, and see if their subscription is valid. If not, it should set a value in the users db, which will then allow you to validate off it.
I've not used Pundit a lot, so the above code would need tweaking a lot. The most basic type of authorization is with roles. The alternative is object-orientated authorization, which will check against the object rather than the user.
If you need "professional help", and if you have a big budget, go to http://anadea.info (I have absolutely no affiliation with them); I worked with them in 2012 and is the reason I've been working with Rails ever since; top quality company.
Don't do it if you don't want to spend $5k+ though, it's not worth it.

Rails How to Login a Guest User with Devise

So I have this app that I'm making where users have profiles after they signup and input their information.
At the moment I'm trying to add a feature that allows for new unregistered users to go to a profile to see what the app is like before they need to sign up (I'm planning on putting a "try it for free" button on the home_controller#index action. For authentication, I'm using the Devise gem.
Currently, I've watched the Railscast (393) on this, but I can't figure out (after several hours of trying) how to implement guest users and log them in using Devise.
I've also read about 4 different solutions on SO, and have decided to stick to this one (how to create a guest user in Rails 3 + Devise):
class ApplicationController < ActionController::Base
def current_user
super || guest_user
end
private
def guest_user
User.find(session[:guest_user_id].nil? ? session[:guest_user_id] = create_guest_user.id : session[:guest_user_id])
end
def create_guest_user
u = User.create(:name => "guest", :email => "guest_#{Time.now.to_i}#{rand(99)}#example.com")
u.save(:validate => false)
u
end
...
end
I have this in my application_controller.rb and don't understand how I would use these functions in the home_controller#index to create a guest user and log into the profile when the "Try it" button is clicked.
I've tried manually creating a user, saving it without authentication and then using Devise's sign_in method on link like so: Try it! and also tried
Try it!
I tried this, but the profile throws some validation messages saying I need to log in to view it. I've also tried removing before_filter authenticate! on the profiles_controller but I can't seem to get this to work at all.
Would anyone know how to create a user on the button click, and auto sign them into a guest profile? Thanks a lot for any help, I'm completely lost here.
I think you have a misunderstanding on what a guest user is. You say that you want guest users to auto sign in, and that is wrong. Guest users can't sign in, because... Well, because they are guests. You want to create them, but not sign them in.
This is why you want these helper methods in your ApplicationController, because when you try to get the current_user, if that doesn't exist (nil), you will have a fallback (that is why you use the || operator), that will assign a guest_user as a current_user.
So, forget about using sign_in links for guest users and you should be fine.

Stop devise from automatically creating a session after a successful registration

I'm using STI and have 2 models inheriting from User.
One type of User can only be created by the other user (Admins create Testers).
The problem is, every time I register a new tester with the admin, it creates a session for the tester.
Perhaps there is a documented way to do this somewhere I haven't found? Can I stop devise from automatically starting a session when I register a new user?
Simple solution: DON'T include the :registerable module in the model. I have it in for Admin, out for Tester.
https://github.com/plataformatec/devise/wiki/How-To:-Redirect-after-registration-(sign-up)
This url may help.In Devise we can create custom methods to do after signup.So, in that function you can clear the session and redirect to some other page which you wanted to.
like,
class NewController < Devise::RegistrationsController
protected
def after_sign_up_path_for(resource)
logout_user
flash[:notice[ = "You have to activate before logging in"
redirect_to some_special_page
end
end

Rails and Authlogic: Allow only one session per user?

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.

Resources