2007-10-28 19:03

ruby on rails, authentication, rails, user, authorizaiton, system, authenticating

User authentication in Ruby on Rails

Autor: Kacper Cieśla (comboy)

This tutorial was written for Ruby on Rails noobs. If you already know something about rails, go straight to the model declaration.

So, first of all, you must be aware that there is a lot of plugins that implement authentication for your rails app. Some of them are:

And there are many more . Some of them looks more like whole application templates rather than just plugins.

Plugins are good and there is no point in writing something that somebody has already wrote, but user authentication isn’t that hard thing to implement. That’s why I prefere to write it myself rather than running through pages of documentation, reading somebody’s dirty code and trying to understand it’s meaning to finally rewrite most of it anyway.

Let’s do it

Let’s start from the scratch:

$ rails userapp

      create
      create  app/controllers
      create  app/helpers
      create  app/models
      (...)

And in our newly created directory userapp edit file config/database.yml, I’ll use MySQL:

development:
  adapter: mysql
  database: test
  username: foo
  password: pass123
  host: localhost

Preparing the user model

We use generator to create user model:

$ ruby script/generate model User

      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/user.rb
      create  test/unit/user_test.rb
      create  test/fixtures/users.yml
      create  db/migrate
      create  db/migrate/001_create_users.rb

And we edit db/migrate/001_create_users.rb, When you’re done it should look like this:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :username, :string
      t.column :password_salt, :string
      t.column :password_hash, :string
      t.column :email, :string
      t.column :created_at, :datetime
    end
  end

  def self.down
    drop_table :users
  end
end

Of course columns email and created_at are optional (btw, created_at is a magic field)

And we run the migration:

$ rake db:migrate
== CreateUsers: migrating =====================================================
-- create_table(:users)
   -&gt; 0.1500s
== CreateUsers: migrated (0.1500s) ============================================

Now it’s time to pimp up our model. First of all, if we set password for an user, it should be saved to columns password_salt and password_hash.

What are those? password_salt is just a random string, and password_hash is a md5 hash of password+password_salt (salt makes it harder to crack).

def password=(pw)
    # we generate a random string
    salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp # 2^48 kombinacji
    # and save salt and the hash
    self.password_salt, self.password_hash =
      salt, Digest::MD5.hexdigest(pw + salt)
  end

Some nice function to check if given password is correct:

def password_is?(pw)
    Digest::MD5.hexdigest(pw + password_salt) == password_hash
  end

And to make it even better we add some validations:

validates_presence_of :username
  validates_uniqueness_of :username

So finally, file app/models/user.rb looks like this:

class User < ActiveRecord::Base

  validates_presence_of :username
  validates_uniqueness_of :username
  validates_confirmation_of :password

  attr_reader :password

  def password=(pw)
    @password = pw # used by confirmation validator
    salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp # 2^48 combos
    self.password_salt, self.password_hash =
      salt, Digest::MD5.hexdigest(pw + salt)
  end

  def password_is?(pw)
    Digest::MD5.hexdigest(pw + password_salt) == password_hash
  end

end

New user registration

This is an example of creating simple rails app based on the given user model, so if you’re exprienced in rails, you can propably skip this and next paragraph.

We need some controller:

$ ruby script/generate controller main
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/main
      exists  test/functional/s
      create  app/controllers/main_controller.rb
      create  test/functional/main_controller_test.rb
      create  app/helpers/main_helper.rb

Now we create app/views/main/register.rhtml with something like this:

<div align="right" style="width: 600px">
<%= '<b>'+flash[:info]+'</b><hr />' if flash[:info] %>
<h3 align="center">New user registration</h3>
<%= error_messages_for 'user' %>
<% form_tag(:action => 'register') do %>
  Login:
  <%= text_field 'user', 'username' %>
  <br /><br />

  Password:
  <%= password_field 'user', 'password' %>
  <br /><br />

  Password again:
  <%= password_field 'user', 'password_confirmation' %>
  <br /><br />

  E-mail:
  <%= text_field 'user', 'email' %>
  <br /><br />

  <%= submit_tag "Register me" %>
<% end %>
</div>

And related action in the controller file app/controllers/main_controller.rb:

class MainController < ApplicationController
  def register
    if request.post?
      @user = User.new params[:user]
      if @user.save
        flash[:info] = 'You are registered now'
      end
    end
  end
end

And it’s done. Easy, right?

After registration user will stay on the registration page so you may want to add some redirection. At the moment we don’t have a good place to redirect to, if we would have it’s enough to add:

redirect_to :action => 'index', :controller => 'main'

Let’s see if it really works:

$ ruby script/server
=&gt; Booting WEBrick...
=&gt; Rails application started on http://0.0.0.0:3000
(...)

And under our favourite browser we open http://localhost:3000/main/register

It does :)

Log in

To go further we need some page that will be availible only for logged in users. In this example I assume that we have some panel controller which is user control panel and should be availible only for registered users. Let’s generate it:

$ ruby script/generate controller panel
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/panel
      exists  test/functional/
      create  app/controllers/panel_controller.rb
      create  test/functional/panel_controller_test.rb
      create  app/helpers/panel_helper.rb

Some tiny form for logging in (app/views/main/login.rhtml):

<%= '<font color="red">'+@auth_error+'</font><hr />' if @auth_error %>
<% form_tag(:action => 'login') do %>
  Login:
  <%= text_field_tag :login, params[:login] %>
  <br /><br />
  Password:
  <%= password_field_tag :password, params[:password] %>
  <br /><br />
  <%= submit_tag "log in" %>
<% end %>

(I haven’t say it will be impressive ;) ). And related action in the main controller:

def login
    if request.post?
      @user = User.find_by_username(params[:login])
	if @user and @user.password_is? params[:password]
	   session[:uid] = @user.id
         redirect_to :controller => 'panel', :action => 'secret'
	else 
	   @auth_error = 'Wrong username or password'
	end
    end
  end

Tadaaaaa…

One detail: Sucessful login ends up with an error… That’s because we don’t have secret action in controller panel.

Checking if user is logged in

User was logged in with this line:

session[:uid] = @user.id

So when we have session[:uid] set then we know user is in. If control panel should be accessible only for registered users, we need to check if session[:uid] is not nil before every action.

There is an elegant way of doing this, and that is before_filter. We need to define a method that check if user is logged in, then add it to the before_filter.

Some simple log out action may be useful too.

When we write it down, our app/controllers/panel_controller.rb looks like this:

class PanelController < ApplicationController

  before_filter :check_auth

  def secret
    render :text => 'This text is only for authenticated users'
  end

  def log_out
    session[:uid] = nil
    flash[:info] = 'You\'re logged out'
    redirect_to :controller => 'main'
  end

  private

  def check_auth
    unless session[:uid]
      flash[:error] = 'You need to be logged in to access this panel'
      redirect_to :controller => 'main', :action => 'login'
    end
  end
end

Now try entering http://localhost:3000/panel/secret

I assume here that you have action index defined in your main controller, which also prints flash[:info] or flash[:error] – I believe you can prepare it by yourself

If you have some problems running it, here is a complete rails app that we’ve made already.

Remembering user

On pages that you visit often, there’s usually very nice option “remember me” when you’re logging in. Let’s try to implement it.

How to do it? We’re going to store a cookie on the client side, informing that given user is logged in. One cookie is enough, but in this implementation we’re going to use two:

  • first cookie is simply a login of the user
  • and the second one is the hash used to authenticate an user

Why some strange hash instead of just password? That’s because of security. I’ve made some assumptions about security:

  • reading cookie by somebody else should not reveal user’s password
  • even if somebody get our cookie, he should not be able to log in after we log out
  • and this cookie should not make it easier for him to guess our next cookie

We’ll need new column to store our cookie hash. Let’s generate a migration:

$ ruby script/generate migration AddUserCookieHash

It looks like this:

class AddUserCookieHash < ActiveRecord::Migration
  def self.up
    add_column :users, :cookie_hash, :string
  end

  def self.down
    remove_column :users, :cookie_hash
  end
end

We run it (rake db:migrate) and add in the logging in form:

<%= check_box_tag :remember %> remember me

Now we modify main controller. To be clear I paste whole log_in action:

def login
    if request.post?
      @user = User.find_by_username(params[:login])
        if @user and @user.password_is? params[:password]
          session[:uid] = @user.id
      	  if params[:remember] # if user wants to be remembered
            cookie_pass = [Array.new(9){rand(256).chr}.join].pack("m").chomp
            cookie_hash = Digest::MD5.hexdigest(cookie_pass + @user.password_salt)
            cookies[:userapp_login_pass] = { :value => cookie_pass, :expires => 30.days.from_now }
            cookies[:userapp_login] = { :value => @user.username, :expires => 30.days.from_now }
	    User.update(@user.id, :cookie_hash => cookie_hash)
          end
          redirect_to :controller => 'panel', :action => 'secret'
        else 
           @auth_error = 'Bad username or password'
        end
    end

So we’ve put some cookies in user’s browser wtih data that lets him authenticate. Now let’s write a code that will authenticate him if those cookies are present (and correct).

We want to log user in independetly of which controller he’ll be starting with. That’s why we’re gonna use our main controller (app/controllers/application.rb)

class ApplicationController < ActionController::Base
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_userapp_session_id'
  before_filter :check_cookie

  def check_cookie
    return if session[:uid]
      if cookies[:userapp_login]
        @user = User.find_by_username(cookies[:userapp_login])
        return unless @user 
        cookie_hash = Digest::MD5.hexdigest(cookies[:userapp_login_pass] + @user.password_salt)
        if @user.cookie_hash == cookie_hash
	  flash[:info] = 'You\'ve been automatically logged in' # annoying msg
          session[:uid] = @user.id
        else 
          flash[:error] = 'Something is wrong with your cookie'
        end
      end
  end

end

Now after restarting the browser user is still logged in.

to be continued (maybe)

your feedback is appreciated


rate this page:

3.0 (11 votes )
Komentarze:
Peter (2007-11-08 12:07)

Awesome tutorial. Thanks! I really hope you post continue this with another.



SeanInSeattle (2007-11-08 16:51)

Hey, this is a great tutorial all around. I learned a lot about how ruby works from reading it, and the code worked right out of the box. Though, I’ve got an issue with the cookies for some reason. Its reporting multiple syntax errors (mostly on the flashing, saying, “syntax error, unexpected kIN, expecting kEND”) where I copied and pasted the code example you gave for application.rb. Any suggestions and/or thoughts?

Your help is already much appreciated through this tutorial. Thanks! – Sean



Kacper Cieśla (comboy) (2007-11-09 19:20)

Thanks for your feedback :) I’ll try to write about implementing some more advanced features soon and include full app code.

@SeanInSeattle: I haven’t check it yet, but there was some error I’ve made while translating this:

flash[:info] = 'You've been automatically logged in'

should be:

flash[:info] = 'You\'ve been automatically logged in'

(It’s already fixed on the page) Hope this work for you, I’ll check it more closely when I’m done with my work.



Ebirm (2007-11-18 02:14)

In the “New User Registration” example, above, you have a password and pw confirmation field, but you never confirm the passwords match during user creation. Can you show us how that’s done?

The example is working great for me, until I try and confirm the passwords match. The confirmation was working prior to implementing the MD5. I was using “validates_confirmation_of”. Thanks a million for your help!!



antonio (2007-11-23 00:43)

Thank you very much! I’ve wanted to learn RoR for a while and I started yesterday and your tutorial was great for this. I suggest you also tag it as a beginner’s tutorial. And having salted encryption in it makes it even better.



Paulo Carvalho (2007-11-30 09:17)

Great tutorial. But i want to ask you one thing. Do you know how can we manage the user permissions with rails? I would like to know how can I control, after a user as logged in, if he can access to the admin interface or not (for example).

Thanks regards



John Loo (2007-12-07 11:28)

I would appriceate some little assistance. I get a ” NoMethodError in MainController#register” due to a “undefined method `password’ for #” when I tried it out. The method is defined as in your example, I even tried to copypaste every single step and still recieve the same problem. Why is that? please help.



Kacper Cieśla (comboy) (2007-12-12 18:03)

Again, thanks for your feedback :) Sorry you had to wait so long for my reply, lately I’m busy as hell.

@Ebirm: I’ve fixed user model (earlier I forgot to add one line), it works fine now

@antonio: thanks :)

@Paulo Carvalho: roles are quite easy to implement, I hope to update article soon.

@John Loo: here you can find sources for the first part of this article, maybe it will help



Łukasz Ś. (2007-12-13 00:31)

świetny tutorial, bez niedopowiedzeń, wszystko opisane krótko, acz rzetelnie i do końca. pozdrowionka



dmitriy (2007-12-14 13:12)

Huge to you thanks. I very long searched for training programs in Russian. And has not found. But has found your program and in Russian already nothing is necessary to me. Enough your training programs. Huge to you thanks. I consider, that manuals such and should be



dmitriy (2007-12-14 14:11)

Tell, whether there are at you still manuals or training програмы on ROR? If is or there are plans to make them, please, write to me. My mail: al_nerov@list.ru



taylor (2007-12-18 09:24)

hey John Loo,

for some reason i was getting the same error, i finally did some readings and decided to change the “attr_reader :password” to

def password @password end

dont know why or how, but it fixed my problem.

it seems that the attribute reader call wasn’t working, so you have to do it manually. curious, are you using rails 2.0?



Mel (2008-01-03 21:43)

2 Questions:

1. Will you update this to use Rails 2.0 and sexy migrations?

2. What about logging out?

I’ve used your tutorial here and some other sources to make ‘login’ a partial so that I can have a login box anywhere on the site and keep with the DRY philosophy. So I’ve edited a few things. It would be really nice if your tutorial were to have a few more pages: panel main page (review their profile), profile editing, and project home page with the login there & logout link. Just a little bit more of a complete system.

Excellent job though, very helpful. Thank you so much for sharing.



janath (2008-01-18 07:14)

it is very usefull if there is test for these functions.



suresh (2008-01-25 12:52)

hi this tutorial is very useful but the thing is i did as what u did upto the login page http://localhost:3000/panel/secret but the problem is i am getting error “Routing Error

No route matches ”/panel/secret” with {:method=>:get}” plz help me urgently to solve this problem



carlivar (2008-02-23 20:41)

No tests?



Kacper Cieśla (comboy) (2008-02-25 16:16)

Sorry that I didn’t respond to any comments, I was pretty busy lately.

I prefere old style of migrations which is still compatibile with new version of rails (and so is this whole tutorial). Logout action – yes, good point – I should have added it, anyway it’s quite simple:

def logout
  session[:uid] = nil
  flash[:info] = "You've been logged out."
  redirect_to :controller => 'main', :action => 'login' # or whatever you want
end

And about tests – there will be no tests :). It’s just a simple tutorial to show how it works, for beginners. People who don’t know much about testing would just find it more complicated, and those who know how to write them don’t need another example of their usage in tutorial that is about something else.

Thanks for your suggestions and feedback, I still hope to update it with roles but right now I’m too busy.



dade- (2008-03-28 14:50)

Just as a note if people are looking at this and need a simple admin login for use with this system. Just add a ‘role’ column to your migration file and you may want to enter these roles yourself, for my app I went with ‘administrator’, ‘developer’, ‘user’. Obviously you want the default to have the ‘user’ role (also an admin will need to be made at the start of the project but this is something that you can execute as you please.

Basically I went with:

def admin_check @user = User.find(session[:uid]) if @user.role == ‘administrator’ @admin = ‘true’ else @admin = ‘false’ end end

And:

before_filter :admin_check

def method(args) if @admin == ‘true’ .... grant access, etc else .... show them the door end end

It’s not perfect, and I’m sure others can think of a more efficient way of executing this, but I did this just quickly. Internal use applications don’t need high security. ;)



Yuuuk (2008-05-23 17:27)

F*



ram (2008-07-24 14:22)

Hi comboy,

thank you for the great tutorial.. fact that u dint use plugins helped in understanding better whats going on! :) I am getting the following error though.. I think that the “password” being accepted in the login form is not a String and is causing the error during verification.. how can i correct this?

TypeError in AccountsController#login

can’t convert nil into String

Application Trace | Framework Trace | Full Trace #{RAILS_ROOT}/app/models/account.rb:21:in `+’ #{RAILS_ROOT}/app/models/account.rb:21:in `password_is?’ #{RAILS_ROOT}/app/controllers/accounts_controller.rb:55:in `login’ /Applications/Bundles/standardRailsMar2007.locobundle/powerpc/bin/mongrel_rails:16:in `load’ /Applications/Bundles/standardRailsMar2007.locobundle/powerpc/bin/mongrel_rails:16



ram (2008-07-24 14:45)

hi,

me again :).. i discovered that the problem is with the password_salt n password_hash.. they’re empty for some reason when i raise an exception and print them.. but when i check in the DB, they have random values as expected.. in urgent need of help!



ram (2008-07-24 15:00)

hi,

me 3rd time.. fixed the problem.. i had made password_salt and password_hash as attr_accessors in the model so that they dont appear when I call the list view (shortcut solution, i know)... i removed that n problem solved!!

it would help if someone could explain why that happens though.. will help in understanding attr_accessor better



Joey (2008-07-27 17:33)

really great tutorial, thanks a lot for this work, it helped me to become a rorkie.

however it would be great if there could be an email-confirmation module which is popular nowadays.



james (2008-08-01 08:12)

very useful to learn how to use custom authentication. Worked fine in rails 2.0!



palash (2008-08-21 06:56)

awesome tutorial…..everything is there, only need some testing in the tutorial….then it will be full prove tutorails





Skomentuj


community.programuj.com
ver. 0.1.93 alpha (bugtrack, ficzers)



Autor tej strony o sobie:

Student AGH (4 rok), autor programuj.com oraz skryptu do tego community. Po jakichś 6 latach PHP przerzuciłem się na Ruby i Railsy. Obecnie zajmuje się głównie frameworlami www, wcześniej pomacałem...


Statystyki (ostatnie 14 dni):
not generated yet>

Kto linka podrzucał (alpha test shit escape):

Pokaż innym:
    del.icio.us Wykop

LOGOWANIE

login:

hasło:

zapamiętaj mnie




Zalogowani:

Wygląda na to że chwilowo pustki:/

Lang_en

  © community.programuj.com - skrypt, pomysł, wykonanie i sprzątanie: Kacper Cieśla (comboy). Niektóre prawa zastrzeżone.
powered by Ruby on Rails, hosted.pl and coffee