Rails ActiveRecord store and new session - ruby-on-rails

I am new to Rails and experience a strange issue I don't understand.
I use ActiveRecord as a session store and need to add session id as a property of JSON responses for all the requests. I use Devise as well if it have some impact on the situation. The problem is that if a request is made by a user without cookies (or at least without session id in the cookie) the session.id is empty or - attention, please - not the same value that is set in the response cookie.
For debugging, I add this code as an after_filter to ApplicationController:
puts session.id
puts request.session_options[:id]
Both values are the same. They match the value in the cookie if it is present. Otherwise, if session id is not present in the cookie, the cookie set after that request has different value.
My opinion is that session_id gets new value after it is actually saved to the database, where it have to be unique. DB migration:
def change
create_table :sessions do |t|
t.string :session_id, :null => false
t.text :data
t.timestamps
end
add_index :sessions, :session_id, :unique => true
add_index :sessions, :updated_at
end
My question: How can I get the actual session.id value of a new session before the first response is rendered?
UPD:
I just created a new Rails app that uses ActiveRecord session store without Devise, and I can get session.id that is going to be set in cookie just before response with this code id application controller:
class ApplicationController < ActionController::Base
after_filter :show_session
def show_session
puts session.id
end
end
But in my existing app with Devise I get a value that really looks like a session id, but that doesn't match the value set in the cookie via Set-Cookie response header and the value actually saved to sessions table in database. Looks like Devise have a conflict with ActiveRecord session store in some way. Need to go deeper to figure it out.
UPD 2
Looks like I found the problem roots. As I said, I use Devise for authorization with Omniauth. According to the documentation, sign_in method resets session id for security reasons. But after that reset session.id returns the old value, that had been automatically set. I use this code as an Omniauth callback:
def facebook_access_token
sign_in #user
puts session.id
end
And in console I get session id different from the one set in the Set-Cookie response header. If I comment "sign_in" line, these values match. New question: how can I get the new session id value after it is been reset inside of sign_in method? Is it an internal Warden/Devise implementation or something?

Renewing is still important and you should not disable it
Also the new session id is generated after the execution of the controller, therefore after you have a chance to set the response to be sent to the client.
The solution is to manually trigger the renewing of the session id
In your ApplicationController add the method:
protected
def commit_session_now!
return unless session.options[:renew]
object = session.options.instance_variable_get('#by')
env = session.options.instance_variable_get('#env')
session_id = object.send(:destroy_session, env, session.id || object.generate_sid, session.options)
session_data = session.to_hash.delete_if { |k,v| v.nil? }
object.send(:set_session, env, session_id, session_data, session.options)
session.options[:renew] = false
session.options[:id] = session_id
end
Then in your controller you just call this method before getting the session id for your response
def my_action
...
commit_session_now!
render json: {session_id: session.id}, status: :ok
end
The code in commit_session_now! comes from Rack::Session::Abstract::ID#commit_session https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L327

The problem I experienced was caused by default Warden configuration. It renewed session id, but somehow the new id was not accessible via session.id.
The only way I found to stop this behavior was putting this code into config/initializers/devise.rb:
Warden::Manager.after_set_user do |user,auth,opts|
auth.env["rack.session.options"][:renew] = false
end
Probably this method is not really good for security reasons, but I have no other ideas in a week of searching and reading sources.

Without knowing the details of your application, my suggestion would be to use a before_filter in your ApplicationController:
class ApplicationController < ActionController::Base
before_filter :use_session_id
protected
def use_session_id
# Do something with session.id
# This will get called before any rendering happens
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

storing session in rails api application

I have a rails api only application [config.api_only = true] in which I enable the cookies through these following lines:
in application.rb:
config.middleware.insert_after ActionDispatch::ParamsParser, ActionDispatch::Cookies
config.middleware.insert_after ActionDispatch::ParamsParser, ActionDispatch::Session::CookieStore
in application_controller.rb
include ActionController::Helpers
include ActionController::Cookies
I also added secret_token.rb as follows:
Rails.application.config.secret_token = 'token'
in my controller, I am trying to store the session like this:
def index
#other codes
session[:userid] = useridstring
render :text => session[:userid]
end
Note: however, after executing this in chrome, I am examining the cookie and none is set...
then in the same controller, but in another action, I am trying to read the session like this:
def readsession
userId = session[:userid]
render :text => userId
end
and nothing is rendered.. :(
Is there anything I missed?
I tried following the answer here which suggest that I set config.api_only = false, however the result is the same (I have no cookie set, and when read in another controller, session is still empty
Sorry that it is such a basic question (or initial configuration matter), I am still very new in ruby and rails..
Since an API is always client independent, so it's best to use a token for authentication.
Here's how:
Add a column called token in users table.
A user comes and logs in.
As he logs in, a token(a string of random characters) is generated, and saved in the database.
The string is passed along as well, and any subsequent request will come with that token.
Since each request comes with a token, you can check the token for its database existence, and association with the right user.
As a user logs out, delete the token from the database.

Session variable getting reset - not sure where it is happening

I am installing Kissmetrics on my rails app by storing events in a session variable and then passing them into the kissmetrics javascript code on the subsequent page. This method works great except for trying to track accounts getting created. It seems that when I store the account created event in my session variable it works fine, but by the time the next page loads, the session variable is gone. I put debugger in there to try to find where it is getting deleted but it seems there's nothing. km_log_event is a method that stores the string in a session variable called km_events. Here's my code:
accounts_controller/create -->
...
if #account.save
log_event("Account", "Created", #account.name)
km_log_event("Account Created")
redirect_to(welcome_url(:subdomain => #account.subdomain))
#user.activate!
#user.add_connection(params[:connect_to])
else
render(:action => 'new', :layout => 'signup')
end
...
sessions_controller/welcome -->
def welcome
if current_account.new?
# Create the session for the owner, the account is brand new
current_account.user_sessions.create(current_account.owner, true)
elsif current_account.users.last && current_account.users.last.created_at > 1.hour.ago
current_account.user_sessions.create(current_account.users.last, true)
end
redirect_to embedded_invitations_path
end
I'm just not sure where it is getting deleted so I can't record this event. It seems to be happening after #account.save in the accounts controller but before the welcome action.
UPDATE:
here is the accounts module where I believe (this isn't my codebase) current_account gets defined.
module Accounts
def self.included(controller)
controller.helper_method :current_account
end
protected
def current_account
return #current_account if defined?(#current_account)
#current_account = Account.find_by_subdomain!(current_subdomain)
end
end
An invalid csrf token will have the session be reset. Could this be happening?
You can test this easily by removing the following from your controller (usually in ApplicationController)
protect_from_forgery
I think this is happening as you are trying to share session between subdomains. To achieve this you have to do some configuration.
Refer Subdomain Session Not Working in Rails 2.3 and Rails 3 on Heroku with/without a Custom Domain?

Rails 3 disabling session cookies

I have RESTful API written on RoR 3.
I have to make my application not to send "Set-Cookie header" (clients are authorizing using auth_token parameter).
I have tried to use session :off and reset_session but it does not make any sense.
I am using devise as authentication framework.
Here is my ApplicationController
class ApplicationController < ActionController::Base
before_filter :reset_session #, :unless => :session_required?
session :off #, :unless => :session_required?
skip_before_filter :verify_authenticity_token
before_filter :access_control_headers!
def options
render :text => ""
end
private
def access_control_headers!
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Headers"] = "Content-type"
end
def session_required?
!(params[:format] == 'xml' or params[:format] == 'json')
end
end
Use the built in option.
env['rack.session.options'][:skip] = true
or the equivalent
request.session_options[:skip] = true
You can find the documentation for it here https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L213
As is mentioned in a comment on John's answer, clearing the session will not prevent the session cookie from being sent. If you wish to totally remove the cookie from being sent, you have to use Rack middleware.
class CookieFilter
def initialize(app)
#app = app
end
def call(env)
status, headers, body = #app.call(env)
# use only one of the next two lines
# this will remove ALL cookies from the response
headers.delete 'Set-Cookie'
# this will remove just your session cookie
Rack::Utils.delete_cookie_header!(headers, '_app-name_session')
[status, headers, body]
end
end
Use it by creating an initializer with the following body:
Rails.application.config.middleware.insert_before ::ActionDispatch::Cookies, ::CookieFilter
To prevent the cookie filter to end up in application stack traces, which can be utterly confusing at times, you may want to silence it in the backtrace (Assuming you put it in lib/cookie_filter.rb):
Rails.backtrace_cleaner.add_silencer { |line| line.start_with? "lib/cookie_filter.rb" }
I'm not sure when they added it to Devise, but there appears to be a configuration that will let you disable the sending of the session cookie when using a auth_token:
# By default Devise will store the user in session. You can skip storage for
# :http_auth and :token_auth by adding those symbols to the array below.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing :skip => :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth, :token_auth]
It does work well. The only issue I had was that I still needed to be able to make an initial request to my token_controller in order to generate/retrieve the token. I.e. POST /api/v1/tokens.json, which unfortunately would cause a session cookie to be returned for that request.
So I ended up implementing the CookieFilter intializer that Ryan Ahearn wrote above anyway.
Also, since my app has both a web front-end as well as a JSON api, I only wanted to filter the cookies for the JSON api. So I modified the CookieFilter class to first check the requests belonged to the api:
if env['PATH_INFO'].match(/^\/api/)
Rack::Utils.delete_cookie_header!(headers, '_myapp_session')
end
Not sure if there's a better way of doing that...
Another solution:
In the controller you want to avoid cookies, add this:
after_filter :skip_set_cookies_header
def skip_set_cookies_header
request.session_options = {}
end
If you have a set of api controllers, set this in an api_controller class and let your other controllers inherit the api_controller.
This skips setting Set-Cookie header since the session opts is empty.
The default CookieSessionStore doesn't send a "Set-Cookie" header unless something is added to the session. Is something in your stack writing to the session? (it's probably Devise)
session :off has been deprecated:
def session(*args)
ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
"Sessions are now lazy loaded. So if you don't access them, " +
"consider them off. You can still modify the session cookie " +
"options with request.session_options.", caller)
end
If something in your stack is setting session info, you can clear it using session.clear like so:
after_filter :clear_session
def clear_session
session.clear
end
Which will prevent the Set-Cookie header from being sent
Further to John's answer, if you are using CSRF protection you would need to turn that off for web service requests. You can add the following as a protected method in your application controller:
def protect_against_forgery?
unless request.format.xml? or request.format.json?
super
end
end
This way HTML requests still use CSRF (or not - depends on config.action_controller.allow_forgery_protection = true/false in the environment).
I myself truly missed being able to declaratively turn off sessions (using session :off)
... thus I brought it "back" - use it just like in plain-old-rails (<= 2.2) :
than of course this might require some additional Devise specific hacking of your own, since session_off might cause session == nil in a controller, and most rails extensions since 2.3 simply assume a lazy session that shall not be nil ever.
https://github.com/kares/session_off
Imo the best approach is to simply remove the cookie session store middleware.
To do so, add this to your application.rb (or to a specific environment if needed):
# No session store
config.middleware.delete ActionDispatch::Session::CookieStore
Try this instead
after_filter :skip_set_cookies_header
def skip_set_cookies_header
session.instance_variable_set('#loaded', false)
end
Or even better, always remove Set-Cookie header when session data did not change
before_filter :session_as_comparable_array # first before_filter
after_filter :skip_set_cookies_header # last after_filter
def session_as_comparable_array(obj = session)
#session_as_comparable_array = case obj
when Hash
obj.keys.sort_by(&:to_s).collect{ |k| [k, session_as_comparable_array(obj[k])] }
when Array
obj.sort_by(&:to_s).collect{ |k| session_as_comparable_array(k) }
else
obj
end
end
def skip_set_cookies_header
session.instance_variable_set('#loaded', false) if (#session_as_comparable_array == session_as_comparable_array)
end
# frozen_string_literal: true
module Api
module Web
module Base
class WebApiApplicationController < ApplicationController
include DeviseTokenAuth::Concerns::SetUserByToken
include Api::Concerns::ErrorsConcern
devise_token_auth_group :user, contains: %i[api_web_v1_user]
respond_to :json
serialization_scope :current_user
before_action :METHOD_NAME
private
def METHOD_NAME
request.session_options[:skip] = true
end
end
end
end
end
It's working for me.

authlogic UserSession.create(#user) giving unauthorized_record

I am trying to create a session explicitly like this UserSession.create(#user, true) but the session is not getting created, current_user is nil.
But when I do this, I get < #UserSession: {:unauthorized_record=>""}>us = UserSession.create(#user, true)
RAILS_DEFAULT_LOGGER.info(us.inspect) #=> UserSession: {:unauthorized_record=>""}
I had a look at Authlogic::Session::UnauthorizedRecord here it says
Be careful with this, because Authlogic is assuming that you have already confirmed that the user is who he says he is. For example, this is the method used to persist the session internally. Authlogic finds the user with the persistence token. At this point we know the user is who he says he is, so Authlogic just creates a session with the record. This is particularly useful for 3rd party authentication methods, such as OpenID. Let that method verify the identity, once it’s verified, pass the object and create a session.
which is exactly what I am trying to do (i am authenticating using omniauth and creating session using authlogic).
How do I fix this, so that I can get a valid session in current_user ?
I had a similar issue caused by the persistence_token being nil on the user. Reset it before creating the UserSession. So...
#user.reset_persistence_token!
UserSession.create(#user, true)
I'm not sure about the .create(object, bool) method signature, but the following works using authlogic.
class Api::ApiBaseController < ApplicationController
protected
def verify_token
return false if params[:token].blank?
#session = UserSession.new(User.find_by_single_access_token(params[:token]))
#session.save
end
end
If that doesn't work for you -- I think the #user isn't being set correctly.
If you map the active_record_store to the authlogic user_sessions table your session information will be stored in the database, and you will be able to store larger sets of data.
Inside your config folder:
config/initializers/session_store.rb
Comment out App::Application.config.session_store :cookie_store, :key => '_App_session'
Add or uncomment App::Application.config.session_store :active_record_store
Inside of config/application.rb
At the end of the class for you application add:
ActiveRecord::SessionStore::Session.table_name = 'user_sessions'
Restart your app, and any information stored in the user session will be saved in the authlogic user_sessions table.
Goto: http://apidock.com/rails/ActiveRecord/SessionStore
For more information
For now you can replace
UserSession.create #user
to
UserSession.create :email => #user.email, :password => #user.password
not a big deal.
But that caught me other way. I forgot that my user got active? == false when created. I've set it to true and session is created.
I ran into this problem today. In my case it ended up being related to CSRF tokens.
We are creating a user and session in our app in response to an OAuth callback. It appears that if the CSRF token is invalid, which would be the case when coming from a third party, authlogic won't create the user session.
Can't verify CSRF token authenticity
The fix was simple:
class Oauth::UserSessionsController < ApplicationController
skip_before_action :verify_authenticity_token, only: :callback
def new
# code removed...
end
def callback
# code removed...
UserSession.create(#user)
redirect_to root_path
end
end

Resources