Creating secure login code in Ruby on Rails

Posted by matt on March 30, 2007

Many web sites require secure user authentication, but a shocking number of them implement it very badly. At the time of posting one of the best ways of dealing with this problem not to store the password in the database, but instead to save a “salt” and a “hash”. The salt is a set of 6 random characters which when combined with the users password and encoded, is equal to the hash. Before going any further, you should be aware that there are plugins for rails which deal with salted logins (http://wiki.rubyonrails.org/rails/pages/SaltedHashLoginGenerator), but I believe it is best to be able to write the code yourself, or at least to have a basic understanding.

user migration

  t.column :username, :string
  t.column :password_salt, :string
  t.column :password_hash, :string

app/models/user.rb:

require 'digest/sha2'
class User < ActiveRecord::Base
  attr_accessor :password

  def password=(pass)
    salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp
    self.password_salt, self.password_hash = salt, Digest::SHA256.hexdigest(pass + salt)
  end

  def self.authenticate(username, password)
    user = User.find(:first, :conditions => [”username = ?”, username])
    if user.blank? || Digest::SHA256.hexdigest(password + user.password_salt) != user.password_hash
      raise “Username or password invalid”
    end
    user
  end
end

application_controller.rb

  def check_authentication
    if session[:user].blank?
      session[:intended_controller] = controller_name
      session[:intended_action] = action_name
      redirect_to :controller => 'user', :action => 'login'
    end
  end

app/controllers/user_controller.rb:

def login
    if request.post?
      begin
        session[:user] = User.authenticate(params[:username],params[:password]).id
        redirect_to :controller => session[:intended_controller], :action => session[:intended_action]
      rescue
        flash[:notice] = "Username or password invalid"
      end
    end
  end

  def logout
    session[:user] = nil
    redirect_to :controller => :user, :action => :index
  end

def register
    if request.post?
        @user = User.new(params[:user])
        if @user.save
          redirect_to :action => :account_creation_success, :id => @user
        end
      end
    end

app/views/user/login.rhtml:


<%= flash[:notice] -%>

<% form_tag :controller => :user, :action => :login do %>
<fieldset>
<label for=”user_username”>Username</label> <%= text_field 'user', 'username' -%>
<label for=”user_password”>Password</label> <%= password_field 'user', 'password' -%>
</fieldset> <%= submit_tag 'Login' -%> <% end %>

app/views/user/register.rhtml:

<% form_tag :controller => :user, :action => :register do %>
<fieldset>
<label for=”username”>Username>/label> <%= text_field_tag 'user', nil, 'username' -%>
<label for=”password”>Password</label> <%= password_field_tag 'user', nil, 'password' -%>
</fieldset> <%= submit_tag 'Register' -%> <% end %>

Now use a before_filter on any controllers where you need to login.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. Paul Carvill Sat, 31 Mar 2007 10:40:32 BST

    Good to see you’re working hard, Matt, even on a Friday. You’re really worth your salt. Arf!

  2. Tony Sun, 01 Apr 2007 23:31:33 BST

    So what’s the difference between copy/pasting all this provided code, and getting the same achieved with running:

    ruby script/generate salted_login

    Sure, you have a better understanding of how things work (and the login generator used to have a bug, I don’t remember if it was fixed since last year). But from reader’s perspective, both are just sets of code.

  3. matt Mon, 02 Apr 2007 07:15:11 BST

    Tony, you are absolutely right. I actually published this post prematurely, with the intention of going back to it within 24 hours to include some explanation of how it works, and what it does. Please accept my apologies, I promise to make the changes very soon!

  4. Tony Mon, 02 Apr 2007 08:43:13 BST

    Ah, that’s.. an odd practice for blogging, but alright. Send me a ping when you update the article, I’ll check it out ;)

  5. Heiko Webers Mon, 02 Apr 2007 16:47:47 BST
  6. hardy Sat, 07 Apr 2007 07:53:34 BST

    Nice to see u that code, But i think u have to also present that example here and give chanse to peple that they can execute that code.. from that u can create more secure than this …..

    by the way thats great

    Hardy

  7. sjs Thu, 12 Apr 2007 19:24:39 BST

    This is just the authentication recipe from Rails Recipes[1] nearly verbatim. At least give credit where credit’s due.

    [1] http://www.pragmaticprogrammer.com/titles/fr_rr/

  8. matt Thu, 12 Apr 2007 21:29:30 BST

    sjs - I have not read Rails Recipes so I can assure you that this is not from it. However, I did put this code together a while ago from reading around on the net. If it is someone else’s work then I apologise. My intention is only to help others with their coding problems and promote rails.

  9. sjs Sun, 15 Apr 2007 19:29:41 BST

    Perhaps that is the idiomatic way to generate salt in Ruby, but that was what led me to think you got it from the recipe. Sorry about that!

  10. sxipjdouuu Thu, 21 Jun 2007 01:44:21 BST

    Hello! Good Site! Thanks you! pvhsrnfkhmg

  11. primordial Sat, 23 Jun 2007 11:46:30 BST

    Nice tutorial, but what’s the database schema behind this code?

  12. matt Tue, 17 Jul 2007 09:22:58 BST

    I have added the database schema in quickly this morning, as well as updating some of the code slightly.

  13. Robert Mon, 06 Aug 2007 10:17:56 BST

    Thanks for the tutorial… it helped me a lot!
    Regards

  14. matt Mon, 06 Aug 2007 21:39:33 BST

    I’m glad I helped you Robert.

    Thanks very much for the workingwithrails recommendation!

  15. Ayyappan K Thu, 23 Aug 2007 22:53:32 BST

    hello,
    i got the following error when i tried the above code to implwment when trying to register.
    SyntaxError in UserController#register

    app/models/user.rb:12: Invalid char `\224′ in expression
    app/models/user.rb:14: Invalid char `\223′ in expression
    app/models/user.rb:14: Invalid char `\224′ in expression

    RAILS_ROOT: ./script/../config/..
    Application Trace | Framework Trace | Full Trace

    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_without_new_constant_marking’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:202:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:94:in `require_or_load’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:248:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:452:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:464:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:260:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:468:in `const_missing’
    #{RAILS_ROOT}/app/controllers/user_controller.rb:20:in `register’

    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_without_new_constant_marking’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:202:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:94:in `require_or_load’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:248:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:452:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:464:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:260:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:468:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `send’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:619:in `perform_action_without_benchmark’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue’
    c:/ruby/lib/ruby/1.8/benchmark.rb:293:in `measure’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/rescue.rb:83:in `perform_action’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `send’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `process_without_filters’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:624:in `process_without_session_management_support’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/session_management.rb:114:in `process’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:330:in `process’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/dispatcher.rb:41:in `dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:113:in `handle_dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:79:in `service’
    c:/ruby/lib/ruby/1.8/webrick/httpserver.rb:104:in `service’
    c:/ruby/lib/ruby/1.8/webrick/httpserver.rb:65:in `run’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:173:in `start_thread’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start_thread’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:95:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `each’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:23:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:82:in `start’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:63:in `dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/servers/webrick.rb:59
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/server.rb:39
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    script/server:3

    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_without_new_constant_marking’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:203:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:202:in `load_file’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:94:in `require_or_load’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:248:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:452:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:464:in `const_missing’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:260:in `load_missing_constant’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:468:in `const_missing’
    #{RAILS_ROOT}/app/controllers/user_controller.rb:20:in `register’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `send’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:619:in `perform_action_without_benchmark’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue’
    c:/ruby/lib/ruby/1.8/benchmark.rb:293:in `measure’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/rescue.rb:83:in `perform_action’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `send’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `process_without_filters’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:624:in `process_without_session_management_support’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/session_management.rb:114:in `process’
    c:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:330:in `process’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/dispatcher.rb:41:in `dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:113:in `handle_dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:79:in `service’
    c:/ruby/lib/ruby/1.8/webrick/httpserver.rb:104:in `service’
    c:/ruby/lib/ruby/1.8/webrick/httpserver.rb:65:in `run’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:173:in `start_thread’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start_thread’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:95:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `each’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:23:in `start’
    c:/ruby/lib/ruby/1.8/webrick/server.rb:82:in `start’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:63:in `dispatch’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/servers/webrick.rb:59
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    c:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require’
    c:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/server.rb:39
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    script/server:3

    Request

    Parameters: {”user”=>{”username”=>”ganesh”, “password”=>”kumar”}, “commit”=>”Register”}

    Show session dump


    flash: !map:ActionController::Flash::FlashHash {}

    Response
    Headers: {”cookie”=>[], “Cache-Control”=>”no-cache”}
    HELP ME TO SOLVE THE ABOVE ERROR.
    Thanks And Regards
    Ayyappan K.

  16. shea Sun, 09 Sep 2007 21:23:52 BST

    Does this prevent the password from being sent naked from the browswer to webserver? I am more concerned about that, than someone hacking my database? I suppose I should use JS to hash it client side?

  17. icicles Fri, 26 Oct 2007 11:44:47 BST

    I am just new to Rails, please did you define action “account_creation_success”. I couldn’t figure it out

Comments