Session hash in rails turning into a string - ruby-on-rails

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])

Related

Devise issue with set_user_by_token

I'm having what may be a simple problem and I cant seem to find a way to fix it though I did find where it is.
What I am trying to do is set up devise password reset with devise_token_auth. I have the email set up with the token generator, and the email links to the end points '/api/auth/passwords/edit' which validates the token and then redirects to my front end password reset form (done in reactjs if it matters), the issue arrises when I actually submit the form, I'm sending the token, I'm also sending the password and confirmation, expiry, uid and all the other headers.
so heres the issue, the devise passwords controller calls the before action set_user_by_token, and i found through some debugging that this is where the issue lies, I created and override module with the default code for the set_users_by_token, and through the use of binding.pry I saw that no values were coming in through the method call, but the method was being called because it hit the pry.
heres the method code
def set_user_by_token(mapping=nil)
# determine target authentication class
rc = resource_class(mapping)
# no default user defined
return unless rc
# gets the headers names, which was set in the initialize file
uid_name = DeviseTokenAuth.headers_names[:'uid']
access_token_name = DeviseTokenAuth.headers_names[:'access-token']
client_name = DeviseTokenAuth.headers_names[:'client']
# parse header for values necessary for authentication
uid = request.headers[uid_name] || params[uid_name]
#token ||= request.headers[access_token_name] || params[access_token_name]
#client_id ||= request.headers[client_name] || params[client_name]
# client_id isn't required, set to 'default' if absent
#client_id ||= 'default'
# check for an existing user, authenticated via warden/devise, if enabled
if DeviseTokenAuth.enable_standard_devise_support
binding.pry
devise_warden_user = warden.user(rc.to_s.underscore.to_sym)
if devise_warden_user && devise_warden_user.tokens[#client_id].nil?
#used_auth_by_token = false
#resource = devise_warden_user
# REVIEW: The following line _should_ be safe to remove;
# the generated token does not get used anywhere.
# #resource.create_new_auth_token
end
end
# user has already been found and authenticated
return #resource if #resource && #resource.is_a?(rc)
# ensure we clear the client_id
if !#token
#client_id = nil
return
end
return false unless #token
# mitigate timing attacks by finding by uid instead of auth token
user = uid && rc.find_by(uid: uid)
if user && user.valid_token?(#token, #client_id)
# sign_in with bypass: true will be deprecated in the next version of Devise
if self.respond_to?(:bypass_sign_in) && DeviseTokenAuth.bypass_sign_in
bypass_sign_in(user, scope: :user)
else
sign_in(:user, user, store: false, event: :fetch, bypass: DeviseTokenAuth.bypass_sign_in)
end
return #resource = user
else
# zero all values previously set values
#client_id = nil
return #resource = nil
end
end
here at the very end its hitting the else and returning resource as nil since no other conditions were met.
Any help would be really appreciated, I'm pretty sure this is where the problems is because I've been debugging for days and this is where it lead me
if youre here because you had the same issue i did hopefully i can help! after hours of grueling debugging and testing I overcame, it might be a workaround or crappy way to do it but it works while still using the devise method.
in your passwords_controller.rb there should be a "before_action :set_user_by_token, only: => [:update]"
change ":set_user_by_token" to what ever you want to name this new method we're going to make, then copy and paste the method in the original post and make some minor changes.
change these lines:
uid = request.headers[uid_name] || params[uid_name]
#token ||= request.headers[access_token_name] || params[access_token_name]
#client_id ||= request.headers[client_name] || params[client_name]
to
uid = params[uid_name]
#token ||= params[access_token_name]
#client_id ||= params[client_name]
and done. now you dont have to mess with any initializers or concers!
hopefully I helped somebody!

Concise way to destroy user's session in Rails

Ok, there appears to be few approaches to 'destroying' a user's session and there may be subtleties between them and how the app handles user sessions.
First, why is it most examples don't use session.delete(:current_user_id) to delete the :current_user_id value (and its hash key!)? A typical example looks like the below (I added deleting :return_to since if signing out, why would there by a need to track a return_to value).
def sign_out
self.current_user = nil
session[:current_user_id] = nil
session.delete(:return_to)
end
If the app needs to delete all session variables and values, isn't it safer to simply use session = nil or session.destroy? This will destroy the hash entirely. It would make sense to keep current_user_id in your session hash if your app supports... say tracking of anonymous users ?!?!
Thoughts?
By setting session to nil you're losing all the information about session (that may also be included except current_user or used by Rails) + you are putting yourself into risk of using a hash method (like #[]) on nil which will raise you exception where you won't expect it.
The proper way to do this is to use the rails method reset_session. If you want to persist certain portion of the session, I would use something like this in your application controller:
def reset_session_with_persistence(*keys_to_persist)
persisted_flash = flash
persisted_keys = keys_to_persist.inject({}) { |keys, key| keys.merge( { key => session[key] } ) }
reset_session
persisted_flash.each { |key, value| flash[key] = value }
keys_to_persist.each { |key_to_persist| session[key_to_persist] = persisted_keys[key_to_persist] }
end

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?

Restful Authentication: Allow logins from multiple computers?

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.

rubyonrails: how to find a session by a session_id

any one know how to find a session by a session_id on RoR?
I'm using Authlogic in my project i don't know if that is correlated
I haven't had to do this myself, and I'm not sure exactly why somebody might need to do this.
Looking through the source code I can see that there might be a way of doing this.
In the Authlogic::Session::Persistence Module there is a find method. You can call this method using UserSession.find and it appears to have the ability to search based on session_id
# This is how you persist a session. This finds the record for the current session using
# a variety of methods. It basically tries to "log in" the user without the user having
# to explicitly log in. Check out the other Authlogic::Session modules for more information.
#
# The best way to use this method is something like:
#
# helper_method :current_user_session, :current_user
#
# def current_user_session
# return #current_user_session if defined?(#current_user_session)
# #current_user_session = UserSession.find
# end
#
# def current_user
# return #current_user if defined?(#current_user)
# #current_user = current_user_session && current_user_session.user
# end
#
# Also, this method accepts a single parameter as the id, to find session that you marked with an id:
#
# UserSession.find(:secure)
#
# See the id method for more information on ids.
def find(id = nil, priority_record = nil)
session = new({:priority_record => priority_record}, id)
session.priority_record = priority_record
if session.persisting?
session
else
nil
end
end
end
The documentation for that method refers to the Authlogic::Session class.
In Authlogic::Session::Session::Config it says that the session key can be a cookie key, a string, or a symbol.
module Config
# Works exactly like cookie_key, but for sessions. See cookie_key for more info.
#
# * <tt>Default:</tt> cookie_key
# * <tt>Accepts:</tt> Symbol or String
def session_key(value = nil)
rw_config(:session_key, value, cookie_key)
end
alias_method :session_key=, :session_key
end
So, in the method that follows, which tries to find the current session, we can see that if the record_id is not nil then it looks up the session using that key.
def persist_by_session
persistence_token, record_id = session_credentials
if !persistence_token.nil?
# Allow finding by persistence token, because when records are created the session is maintained in a before_save, when there is no id.
# This is done for performance reasons and to save on queries.
record = record_id.nil? ?
search_for_record("find_by_persistence_token", persistence_token) :
search_for_record("find_by_#{klass.primary_key}", record_id)
self.unauthorized_record = record if record && record.persistence_token == persistence_token
valid?
else
false
end
end
record_id is created with the session_credentials method. Which seems to build a session key based on the key provided to the controller
def session_credentials
[controller.session[session_key], controller.session["#{session_key}_#{klass.primary_key}"]].compact
end
def session_key
build_key(self.class.session_key)
end
I gathered most of this by browsing through the source at Github. If you need more help, that may be the best place to start looking.
Hope this helps

Resources