Ruby on Rails
HowtoAuthenticate

This article is part of the confusing world of Authentication in Rails. Feel free to get lost in a gazillion of nearly useless and/or outdated Wikipages.

Heya, almost the same stuff can be found under quick_and_dirty_homemade_authentication_solution" class="existingWikiWord">A_quick_and_dirty_homemade_authentication_solution._

The controller that needs protection:


class WeblogController < ActionController::Base
  before_filter :authenticate

  def index
    # show the secret stuff
  end

  protected
    def authenticate
      unless @session["person"]
        @session["return_to"] = @request.request_uri
        redirect_to :controller => "login"
        return false
      end
    end
end

The controller that does login:

class LoginController < ActionController::Base
  def index
    # show login screen
  end
  
  def authenticate
    if @session["person"] = Person.authenticate(@params["name"], @params["password"])
      if @session["return_to"]
        redirect_to_path(@session["return_to"])
        @session["return_to"] = nil
      else
        redirect_to :controller => "weblog"
      end
    else
      flash["alert"] = "Login failed!"
      redirect_to :action => "index"
    end
  end

  def logout
    reset_session
    flash["alert"] = "Logged out"
    redirect_to :action => "index"
  end
end

The model that does authentication:


class Person < ActiveRecord::Base
  def self.authenticate(name, password)
    find(:first,
      :conditions => [ "name = ? AND password =?", name, password ]
    )
  end
end

Notice that in this example there is no password encryption used. However its very easy to add :


require 'digest/sha1'

class User < ActiveRecord::Base

  def self.authenticate(email, password)
    find(:first, 
      :conditions => ["email = ? and password = ?", email, Digest::SHA1.hexdigest(password)]
    )
  end

  def password=(str)
    write_attribute("password", Digest::SHA1.hexdigest(str))
  end

  def password
    ""
  end
end

A digression on hash functions

Don’t forget that your password fields have to take 40 char long strings for SHA1 and 32 char strings for MD5. MD5 was recently cracked ( by way of a collision ) so SHA1 is the thing to do now.

Please see the HashFunctionDebate if you are generally interested in cryptographic mechanisms to create message digests. The discussion has very little (if anything) to do with using MD5 or SHA1 to create a signature.


You can also do AuthenticationWithAbstractApplicationController


example: keeping the above example in mind, the easiest way to do that would probably be something like this:


def authenticate if person = Person.authenticate(@params["name"], @params["password"]) @session["person_id"] = person.id # rest of code #

- jsdk


Sanitize

If you have a user object with a lot of associated objects (has_many :posts), the session can get very large if you put the whole user object in it. I usually only save the user_id in the session and create the user object in a before_filter.

Which means you add a round-trip to database on each request _for no apparent good reason. Better to keep associations out of user object, IMHO. —AlexeyVerkhovsky _

One additional DB request does rarely affect the performance (the time the database needs to process a simple primary key select is negligable anyway). And why should I keep associations out of the user object if there are associations in the data structure? Furthermore I don’t want expired user data to continue living inside a session.
That’s an interestng point —Alex


Comments

Comment: If you’re like me, you’re crazy about security. So I wrote my own session system that stores the user ID and encrypted password in MySQL and cookies. On each page view, it verifies that they match. It prevents session hijacking via brute force in 99.99% of cases, if not 100% of cases, since the hijacker would require all 3 variables to successfully hijack a session. I also destroy the session if the variables don’t match, since it’s a possibility that it’s been hijacked. This may be an option for you to consider if you’d like very secure sessions, but it depends on your application. If you can sacrifice performance for security (it takes some time to do a query against the database) like I can, it’s a great method.
— Eddie

Comment: This page is a great help; however I found that I was getting session restore errors like this: “ActionController::SessionRestoreError: Session contained objects where the class definition wasn’t available. Remember to require classes for all objects kept in the session. The session has been deleted. (Original exception: undefined class/module User [ArgumentError])
and therefore had to specify model :person in my equivalent of this WeblogController? to make it work.
DjAdams

Comment: I had the same problem. The webbrick will all of a sudden start throwing up 404s. You can delete the ruby session to see pages again or just do what he says and add model :person. I had to add that line to weblog_controller to get login to work, and logincontroller to get logout to work.
— SM

Comment: The “reset_session” call above seems to invalidate the following flash statement, so it doesn’t take effect. Either say @session“person” = nil, or drop the flash.
Lars Pind?

Comment: The protected call now requires you to make sure your other methods are public. Authentication works without the protected call on 0.9.5 for me.
khudgins

Comment: Overloading the password methods as above will break validations when using validates_confirmation_of to check password fields. You will need to add these methods for the password_confirmation field.
- JasonStirk

Comment: You might want to reconsider storing the straight SHA1-encoded passwords in your database. Few people pick good passwords, and thus if an attacker (or disgruntled employee) ever gets a copy of your database something the daily news suggests is more common than we would like to hope dictionary-based attacks are likely to reveal many of the encoded passwords. Further, because there is a one-to-one mapping from clear passwords to your encoded representation, an attacker can attack all accounts simultaneously and even use a precomputed dictionary.

To reduce the risk of these problems, I combine the password with a random salt, compute the digest of the combination, and then store both the salt and the digest in the database. The following code shows one way to do it, using a Base64-encoded 48-bit salt:


  def password=(pw)
    salt = [Array.new(6){rand(256).chr}.join].pack("m")[0..7]; # 2^48 combos
    # password_salt and password_sha1 are DB-backed AR attributes
    self.password_salt, self.password_sha1 =
      salt, Digest::SHA1.hexdigest(pw + salt)
  end

  def password_is?(pw)
    Digest::SHA1.hexdigest(pw + self.password_salt) == self.password_sha1
  end

Tom Moertel

Comment:
Tom,

How is this anymore protection if someone gets at your database? They’d have access to the salt value and could run their cracking against the DB using the salt +pw combination to do the match. I guess you’ve added another layer to the process but really the salt +password combo can show up in one of two ways: salt + pw or pw + salt. Just curious as to why this method is more secure.

If I know you use straight sha1 for your passwords, I can precompute a list of sha1 applied to every word in the dictionary, then scan your db for those values.
If each password has a different salt, then I can’t use my precomputed list anymore – I need to run sha1 on my entire dictionary plus the salt of your first password. Then I have to do this for each user… Much more processing effort involved.
peter

This is a common mechanism that has been used in UNIX password schemes for a long time. See http://www.usenix.org/events/usenix99/provos/provos_html/node10.html for detals.

If someone has gained access to the database, can’t he just manually set all salts and password to some value and then gain acces to all accounts? Another quesstion: if the database has been cracked, what’s left to protect there?
— Benol

There’s a difference between obtaining a copy of the database and having write access to the database on the production server. If an attacker has a copy of the users table you still want to prevent him from logging in to the live server as other users. — Steven Soroka


Questions & Answers

Q:Does using MySQL SHA1 function causes plain-text passwords to show up in the errorlog?

A: Actually the plain-text password will only show up in the mysql query log. Activating a query log in a production enviroment could be considered a bad thing no matter what. The query log is written as the mysql user, so normal users won’t be able to read this log, still not a very good idea though.


Q: The SQL shown allows SQL injection, doesn’t it?
A: No, there is no SQL injection because it is using a prepared query (the ? in the query). Using something like find_first “user=#{user}” would allow SQL injection.

A: bq. The queries on this page have been updated to make use of Rails automatic value sanitizing. You can read more about it on the ActiveRecord API page, under ‘Conditions’
- LeeO


Q: What is the best please to use @session‘user’.id to ensure that users are only editing records that they own? Something like:

Post.delete_all ‘id=? AND user_id=?’, @params‘id’, @session‘user’.id]

Or is there a better way?

A: I’m rather new but this is what I do. If a user has many posts, and a post belongs to one user. then you can do something like this in your controller.

@user = get_user
@post = @user.posts.find(params[:id])

if the post doesnt belong to the user it can’t be retrieved. If anyone knows of a better way I’d def be interested to hear it though. =D

Passwords show up in clear text in the logs


Q: Don’t plain-text passwords still show up in the access log files as part of the POST requests? Anyone know how to prevent that?

A: The previous answers for this question were perhaps out of date. They were suggesting adding a bunch of methods for a bit of functionality Rails already provides.

To mask out sensitive fields from the log files, use the filter_parameter_logging in your controllers.

filter_parameter_logging :password, :password_confirmation, :decrypted_number, :number

oksteev


Q: How would, using this scheme, one create a list of authenticated or logged in users?

A: On login, have the authenticate method update a field in the db like, last_logged_in_at.


Q: How would you require authentication of a plugin? Adding a before_filter to one’s own controllers is easy, but how do you wrap a plugin with a similar filter?

A: …?