Devise issue with set_user_by_token - ruby-on-rails

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!

Related

Session hash in rails turning into a string

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

Get password inside authenticate_or_request_with_http_digest

I have an app which connects to an iphone app, which in turn authenticates it's users via http_digest.
I'm using authlogic, and in my schema users of the website are "users" and users of the phone app are "people". So, i have user_sessions and people_sessions. To handle the http_digest auth, i'm using the authenticate_or_request_with_http_digest method like this:
def digest_authenticate_person
authenticate_or_request_with_http_digest do |email, password|
#ldb is just a logging method i have
ldb "email = #{email.inspect}, password = #{password.inspect}"
person = Person.find_by_email(email)
if person
ldb "Authentication successful: Got person with id #{person.id}"
#current_person_session = PersonSession.create(person)
else
ldb "Authentication failed"
#current_person_session = nil
end
return #current_person_session
end
end
I can see in the logs that password is nil: only email is passed through to the inside of the authenticate_or_request_with_http_digest block.
Im testing this with a curl call like so:
curl --digest --user fakename#madeup.xyz:apass "http://localhost:3000/reports.xml"
I'd expect "fakename#madeup.xyz" and "apass" to get passed through to the inside of the block. Once i have the password then i can use a combination of email and password to find (or not) a user, in the normal way. Does anyone know how i can get access to the password as well?
grateful for any advice - max
EDIT - on further googling, i think i'm using this method wrong: i'm supposed to just return the password, or the crypted password. But then how do i compare that against the password passed as part of the http_digest username?
Found the answer: i had a fundamental misunderstanding of how authenticate_or_request_with_http_digest works: after reading the documentation (in the source code of the gem) i realised that the purpose of this method is not to do the authentication, its purpose is to provide the "email:realm:password" string to the browser, let the browser encrypt it, and check the result against it's own calculated (or cached) version of this.
Here's how i set it up:
def current_person
if #current_person
#current_person
else
load_current_person
end
end
#use in before_filter for methods that require an authenticated person (mobile app user)
def require_person
unless current_person
redirect_to root_path
end
end
def load_current_person
#check user agent to see if we're getting the request from the mobile app
if request.env['HTTP_USER_AGENT'] =~ /MobileAppName/
result = digest_authenticate_person
if result == 401
return 401
elsif result == true
#make authlogic session for person
#current_person_session = PersonSession.new(#person_from_digest_auth)
#current_person = #person_from_digest_auth
end
end
end
#this method returns either true or 401
def digest_authenticate_person
authenticate_or_request_with_http_digest(Person::DIGEST_REALM) do |email|
person = Person.find_by_email(email)
#result = nil
if person
#need to send back ha1_password for digest_auth, but also hang on to the person in case we *do* auth them successfully
#person_from_digest_auth = person
#result = person.ha1_password
else
#person_from_digest_auth = nil
#result = false
end
#result
end
end

Converting existing password hash to Devise

I'm trying to convert an existing Admin model to Devise. We already have a password hash but it's obviously not Devise compatible. What I would like to do is accept the login form and check the provided password against the encrypted password. If it's not correct, use the old hash to check the password and if it matches, empty the old password_hash field and set Devise's password to the provided password and save the model.
What's the best way to move forward? I suspect that I need to override something, perhaps in a custom controller, but I'm not entirely sure how to proceed.
You can let Devise do the "hard work" of encrypting the password with the new crypt scheme, as shown in https://gist.github.com/1704632:
class User < ActiveRecord::Base
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
super(password)
rescue BCrypt::Errors::InvalidHash
return false unless Digest::SHA1.hexdigest(password) == encrypted_password
logger.info "User #{email} is using the old password hashing method, updating attribute."
self.password = password
true
end
end
end
Using the bcrypt encryptor in Devise, this is what I ended up doing with my legacy data:
In models/user.rb
# Because we have some old legacy users in the database, we need to override Devises method for checking if a password is valid.
# We first ask Devise if the password is valid, and if it throws an InvalidHash exception, we know that we're dealing with a
# legacy user, so we check the password against the SHA1 algorithm that was used to hash the password in the old database.
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
Digest::SHA1.hexdigest(password) == encrypted_password
end
end
As you can see, devise throws an InvalidHash exception when it encounters an invalid hash, which it would do when authenticating a legacy user.
I use this to fall back to the hashing-algorithm used to create the original legacy hash.
It doesn't change the password though, but that could simply be added to the method if needed.
First you need to copy password_salt and encrypted_password to your new object model
Using this because I have to export my database User to another application and old,
app are using devise 1.0.x and new app using 2.1.x
Class User < ActiveRecord::Base
alias :devise_valid_password? :valid_password?
def valid_password?(password)
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
salt = password_salt
digest = nil
10.times { digest = ::Digest::SHA1.hexdigest('--' << [salt, digest, password, nil].flatten.join('--') << '--') }
digest
return false unless digest == encrypted_password
logger.info "User #{email} is using the old password hashing method, updating attribute."
self.password = password
self.password_salt = nil # With this you will knew what object already using the new authentication by devise
self.save
true
end
end
end
If you're moving from SHA512, the solution is a bit more involved than moeffju's SHA1 solution:
def valid_password?(password)
if has_legacy_password?
return false unless valid_legacy_password?(password)
convert_legacy_password!(password)
true
else
super(password)
end
end
protected
def has_legacy_password?
password_salt.present?
end
def convert_legacy_password!(password)
self.password = password
self.password_salt = nil
self.save
end
def valid_legacy_password?(password)
stretches = 10
salt = password_salt
pepper = nil
digest = pepper
stretches.times do
tokens = [salt, digest, password, pepper]
digest = Digest::SHA512.hexdigest('--' << tokens.flatten.join('--') << '--')
end
Devise.secure_compare(encrypted_password, digest)
end
Be sure to replace stretches and pepper with the values you used to encrypt the passwords.
follow Thomas Dippel instructions i have made a gist that update password:
https://gist.github.com/1578362
# Because we have some old legacy users in the database, we need to override Devises method for checking if a password is valid.
# We first ask Devise if the password is valid, and if it throws an InvalidHash exception, we know that we're dealing with a
# legacy user, so we check the password against the SHA1 algorithm that was used to hash the password in the old database.
#SOURCES OF SOLUTION:
# http://stackoverflow.com/questions/6113375/converting-existing-password-hash-to-devise
# https://github.com/binarylogic/authlogic/blob/master/lib/authlogic/crypto_providers/sha512.rb
# https://github.com/plataformatec/devise/blob/master/lib/devise/encryptors/authlogic_sha512.rb
alias :devise_valid_password? :valid_password?
def valid_password?(password)
debugger
begin
devise_valid_password?(password)
rescue BCrypt::Errors::InvalidHash
stretches = 20
digest = [password, self.password_salt].flatten.join('')
stretches.times {digest = Digest::SHA512.hexdigest(digest)}
if digest == self.encrypted_password
#Here update old Authlogic SHA512 Password with new Devise ByCrypt password
# SOURCE: https://github.com/plataformatec/devise/blob/master/lib/devise/models/database_authenticatable.rb
# Digests the password using bcrypt.
# Default strategy for Devise is BCrypt
# def password_digest(password)
# ::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
# end
self.encrypted_password = self.password_digest(password)
self.save
return true
else
# If not BCryt password and not old Authlogic SHA512 password Dosn't my user
return false
end
end
end

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