views:

571

answers:

2

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?

A: 

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.

erik
Hmmm, what version of Restful Authentication are you referring to? I'm using a pretty recent version and `remember_token` is set with a lot more complex serious of methods and SHA1 encryption.
Shpigford
Ah, sorry. This is a pretty old version that I've had running for at least a year. Didn't realize it had changed so much.
erik
+6  A: 

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.

jcnnghm
Thanks a ton!So how does the functionality of a user checking "Remember me" work now? Does it still remember them for the amount of time set in the `remember_me` method?
Shpigford
It still remembers them for 2 weeks, as in the remember_me method, but that 2 weeks starts the first time the token is used. In other words, if you log in from computer A, then 10 days log in from computer B, 4 days later the token expires on both computers.
jcnnghm
Great. Thanks again for your help!
Shpigford