martes, 3 de julio de 2012

Authentication in Rails From Scratch

Today, we’re going to learn how to implement a simple user authentication system in a Rails application from scratch. Along the way, we’ll examine best practices to help avoid common – and costly – mistakes.


Step 1: Introduction to User Authentication

Password-protected actions are a common feature in most web applications. We’ll only allow users with a valid username and password to access these actions. This is referred to as “User Authentication,” which most Rails applications will require in some form or another. Let’s begin with a quick scenario of how the process works.

  • Signup: To begin, we will create a new user in the database. We’ll need to obtain the username, password (which will be encrypted in the database), email address, and other miscellaneous details from the author.
  • Login: The user will be able to login with her/his username and password. The authentication process takes place by matching the supplied credentials with what is stored in the database. If the credentials don’t match, the user should be redirected to the login page.
  • Access Restriction: We’ll create a session to hold the authenticated user’s ID, after login. This way, navigation through additional protected actions can be done easily by simply checking the userID in the session.
  • Logout: Finally, the logout process, in which we set the authenticated userID in the session file to Nil.

Step 2: Generate the User Model & Controller

First, let’s create our application, User_Auth, and pass MySQL as our database.

          $ rails new User_Auth -d mysql            

Next, we’ll navigate to the application directory to generate the User Model and Controller with the new method as a parameter.

          $ rails cd ./User_Auth          $ rails g model user          $ rails g controller users new            

Step 3: Setup the Database

Now we need to create the Users table within the database. To accomplish this, we’ll add the user attributes to a new migration.

                  class CreateUsers < ActiveRecord::Migration                    def change                      create_table :users do |t|                          t.string :username                          t.string :email                          t.string :encrypted_password                          t.string :salt                          t.timestamps                      end                    end          end            

We’ve added four columns to the Users table: username, email, encrypted_password, and salt. Remember that we should never store passwords in plain text; the values should be encrypted first before saving to the database. There are a variety of different encryption techniques, some of which we’ll review in this tutorial.

After we’ve created the model and migration successfully, we can create the database and migrate it to create the Users table within the database.

                  $ rake db:create                  $ rake db:migrate            

Step 4: Creating New User Actions

Next, let’s write the new and create actions in the Users Controller.

                  class UsersController < ApplicationController                                  def new                                          @user = User.new                                  end                                  def create                                          @user = User.new(params[:user])                                                  if @user.save                                                     flash[:notice] = "You Signed up successfully"                                                     flash[:color]= "valid"                                                  else                                                     flash[:notice] = "Form is invalid"                                                     flash[:color]= "invalid"                                                  end                                          render "new"                                  end                          end                            

Above, we’ve created two main actions:

  • new action
  • create action, which creates the user, based on the parameters passed from the new template.

Step 5: Sign Up Form Template

Before creating the signup form, I’ve created a simple layout in the application.erb file, inside views/layout directory.

Now, let’s write the signup form inside the new template.

                                  <% @page_title = "UserAuth | Signup" %>                                  <div class= "Sign_Form">                                          <h1>Sign Up</h1>                                          <%= form_for(:user, :url => {:controller => 'users', :action => <code>create</code>}) do |f| %>                                                    <p> Username:</br> <%= f.text_field :username%> </p>                                                    <p> Email:</br> <%= f.text_field :email%> </p>                                                    <p> Password:</br> <%= f.password_field :password%></p>                                                    <p> Password Confirmation:</br> <%= f.password_field :password_confirmation%> </p>                                                  <%= f.submit :Signup %>                                          <% end %>                                            <% if @user.errors.any? %>                                                  <ul class="Signup_Errors">                                                          <% for message_error in @user.errors.full_messages %>                                                             <li>* <%= message_error %></li>                                                          <% end %>                                                  </ul>                                          <% end %>                                  </div>                            

Here, we’ve created a form for signing up, which accepts a username, email, password and confirmation password from the user. This information will be sent as params[:user] to the create action. Additionally, we’ve added an if statement to check for errors, just in case the user enters invalid information.

You also might have noticed that the signup form has two fields, password and password_confirmation, that must match in the user table in our database. We need to add the attr_accessor methods in the User Model to handle these.


Step 6: Adding Some Validation to the User Model

In addition to attr_accessors, we need to add some validation rules to ensure that the input data matches the requirements.

                          class User < ActiveRecord::Base                                  attr_accessor :password                                  EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i                                    validates :username, :presence => true, :uniqueness => true, :length => { :in => 3..20 }                                  validates :email, :presence => true, :uniqueness => true, :format => EMAIL_REGEX                                  validates :password, :confirmation => true #password_confirmation attr                                  validates_length_of :password, :in => 6..20, :on => :create                          end                    

We’ve created a signup page that creates a new user and validates the input data, but we didn’t convert the password from ordinary text into the encrypted format. Let’s talk about password encryption methods for a bit.


Step 7: Password Encryption Technique

As noted above, never try to store passwords in the database as plain text. If you do, anyone who manages to obtain access to the database will gain access to every user’s password.

Hashing Password

“Salt” is an additional string of data that is added to the password before encrypting it.

Hashing is the process of applying mathematical functions and algorithms to a string of data to produce a unique string. While creating a new user, the plain text password is hashed, and then saved to the database. During the login process, the input password is hashed and compared to the hashed password within the database. This technique is referred to as one-way encryption.

We can implement a hashed method using SHA1 in Rails with just two lines of code.

                                  require 'digest/sha1'                                  encrypted_password= Digest::SHA1.hexdigest(password)                            

Salting Password

A few drawbacks do exist with hashing passwords – for example, Rainbow tables. Salting the password is a more advanced technique, which provides more security for the environment. “Salt” is an additional string of data that is added to the password before encrypting it. It’s best to make this salt unique and random, to make the Rainbow tables useless. This way, each user’s hash is random and entirely unique.

                                  salt= Digest::SHA1.hexdigest("# We add {email} as unique value and #{Time.now} as random value")                                  encrypted_password= Digest::SHA1.hexdigest("Adding #{salt} to {password}")                            

Bcrypt

There’s another easy way to encrypt passwords, using Bcrypt. bcrypt-ruby is a Ruby gem for password encryption, which we’ll make use of in our application.

To install it, add the following reference to your gem file.

                                  gem 'bcrypt-ruby', :require => 'bcrypt'                            

…and inside your application directory, run:

                                  $ bundle install                            

Now, we can make use of the gem by doing:

                                  salt = BCrypt::Engine.generate_salt                          encrypted_password = BCrypt::Engine.hash_secret(password, salt)                            

Step 8: Callbacks

We now need two functions: one to encrypt the actual password (plain text) before saving a user record, and the other function to assign the password accessor_attr to nil.

Now, let’s add these functions and callbacks to the User model.

                          before_save :encrypt_password                          after_save :clear_password                            def encrypt_password                                  unless password.blank?                                          self.salt = BCrypt::Engine.generate_salt                                          self.encrypted_password= BCrypt::Engine.hash_secret(password, salt)                                  end                          end                            def clear_password                                  self.password = Nil                          end                    

Step 9: Mass Assignment Protection

One of the most common security issues relates to how to protect some attributes during mass assignment.

In the create action, rather than directly assigning each attribute one by one, we’ll just drop a hash of all the values that we want to assign to the attributes. This is called mass assignment. A security issue is present here, though, since we don’t know anything about what these values actually contain. In case a malicious string is added to the URL, it will be assigned to the attributes.

To avoid it, we have two methods in Rails.

  • attr_protected: specifies that all attributes should be ignored during mass assignment, and all other attributes will be accessible.
  • attr_accessible: makes attributes accessible during mass assignment, and all other attributes will be protected.

Finally, let’s add the accessible attributes into the User model.

                                  attr_accessible :username, :email, :password, :password_confirmation                            

We’re finished with the signup process! You can run your server, sign up to create a new user, and test it to ensure that the password is encrypted in the database successfully!

Note: Don’t forget to add a default route to the routes file. You can just un-comment the following line at the end of the file.

                                  match ':controller(/:action(/:id))(.:format)'                            

Step 10: Authentication Method

As we can now save encrypted passwords in the database, it’s time to setup an authentication method, which takes a username/email and password, to determine if they match the user in the database. What we need now is a query for matching the username/email, and, if found, we’ll encrypt the password and compare it with the encrypted password in the database.

Let’s write an authentication method in the User model.

                                  def self.authenticate(username_or_email="", login_password="")                                            if  EMAIL_REGEX.match(username_or_email)                                                  user = User.find_by_email(username_or_email)                                          else                                                  user = User.find_by_username(username_or_email)                                          end                                            if user && user.match_password(login_password)                                                  return user                                          else                                                  return false                                          end                                  end                                       def match_password(login_password="")                                          encrypted_password == BCrypt::Engine.hash_secret(login_password, salt)                                  end                            

If login_password matches, we return the user object, and if it does not match or the username/email is not found, we return false.


Step 11: The Sessions Controller

Let’s generate the sessions controller, which will create the login, home, profile and setting templates.

                                  $ rails g controller sessions login, home, profile, setting                            

We’ll focus on the login template for now. We’ve created a form that accepts a username/email and password from the user, and passes it to the login_attempt action.

                                  <% @page_title = "UserAuth | Login" -%>;                                  <div class= "Sign_Form">;                                          <h1>Log in</h1>                                          <%= form_tag(:action => 'login_attempt') do %>                                                  <p>Username or Email:</br> <%= text_field_tag(:username_or_email) %></p>                                                  <p>Password:</br> <%= password_field_tag :login_password %>;</p>                                                  <%= submit_tag("Log In") %>                                          <% end %>                                  </div>                            

Step 12: Creating the Login_attempt Action

We’re going to create the login_attempt action that takes the params from the login form and authenticates it, using the authentication method. If the user is logged in successfully, we redirect them to the home action. If not, we’ll render the login template once again.

                          class SessionsController < ApplicationController                                    def login                                          #Login Form                                  end                                    def login_attempt                                          authorized_user = User.authenticate(params[:username_or_email],params[:login_password])                                          if authorized_user                                                  flash[:notice] = "Wow Welcome again, you logged in as #{authorized_user.username}"                                                  redirect_to(:action => 'home')                                          else                                                  flash[:notice] = "Invalid Username or Password"                                                  flash[:color]= "invalid"                                                  render "login"                                          end                                  end                            end  

There is something missing here: as you might see, it is the ability to save the state of the user. We need a session to mark the user’s state, so that we can just check it for each of his subsequent requests.

Let’s see how we can do this.


Step 13: Cookies, Sessions and Super-Cookies

Cookies:

We know that, when a user send a request to the web server, it is treated as a new request – the web server doesn’t know about the previous requests. A simple solution is cookies.

You can make use of cookies in Rails quite easily:

                                  cookies[:name]= "Azzurrio"                            

Sessions:

Due to some limitations in cookies, such as the 4kb max size, the web server sends an ID in a cookie file to the browser, which saves it. The browser can now send the cookie data with each request to that web server, which uses the cookie’s ID to locate the session file.

We can use sessions in Rails, like so:

                                  session[:name]= "Azzurrio"                            

Super-Cookie:

We can save the session file in the file storage or database, but these options aren’t fast, don’t scale, and require multiple database calls. Instead, we will use cookies for storage, since it’s a quick and safe solution. We’ll make a super-cookie, and put the session in it, after encryption, to ensure that the user cannot read or alter it.

Session Configuration:Inside the config/initializers folder, you can find two configuration files to configure your sessions. The first is session_store.rb, to configure the storage option you want to use (and we will see that cookie_store is the default option), and the second is secret_token.rb, which contains the string that Rails uses to encrypt the cookie file.

Now, let’s get back to our application and save the login state in the session, if the user is authorized in login_attempt action.

                                  def login_attempt                                          authorized_user = User.authenticate(params[:username_or_email],params[:login_password])                                          if authorized_user                                                  session[:user_id] = authorized_user.id                                                  flash[:notice] = "Wow Welcome again, you logged in as #{authorized_user.username}"                                                  redirect_to(:action => 'home')                                          else                                                  flash[:notice] = "Invalid Username or Password"                                                  flash[:color]= "invalid"                                                  render "login"                                          end                                  end                            

Always store an ID that refers to the object in the session file, not the object itself.

Here, we’ve created a user_id session, and have stored the authorized user id within it – so we can just check it when a user requests additional actions.

Note:It is a good practice to always store an ID that refers to the object in the session file, not the object itself as it is large and will increase the time to retrieve it. Instead, you can get a reference to it by storing the ID and you can then use it to get a copy of the object from the database when you need it.


Step 14: Access Restriction

We need to check the session file every time the user requests some protected action. To do this, we use the before_filter method.

The before_filter is a method, which performs a function before the specific action is executed. It’s a bit like callbacks, but the core difference is that the before_filter is for controllers, while callbacks are for models.

We can use it by passing it the function name that we want to perform right before the actions are executed. The second parameter is a list of actions that we want to apply the function to. Let’s add it to our application now.

                          protected                          def authenticate_user                                  unless session[:user_id]                                          redirect_to(:controller => 'sessions', :action => 'login')                                          return false                                  else                                          # set current user object to @current_user object variable                                          @current_user = User.find session[:user_id]                                          return true                                  end                          end                            def save_login_state                                  if session[:user_id]                                          redirect_to(:controller => 'sessions', :action => 'home')                                          return false                                  else                                          return true                                  end                          end                    

In the authenticate_user method, we determine if the user_id session is available. If so, we assign the user object to the @current_user variable, and return true, which means that the action will be executed. Otherwise, we’ll return false, and redirect the user to the login page. The other method, called save_login_state, is used to prevent the user from accessing the signup and login pages without logging out.

Next, add before_filter to Sessions controller, like so:

                          before_filter :authenticate_user, :only => [:home, :profile, :setting]                          before_filter :save_login_state, :only => [:login, :login_attempt]                    

.. and to the Users controller:

                          before_filter :save_login_state, :only => [:new, :create]                    
                          <h2 class='User_Header'> <%=@current_user.username%> Profile <h2>                    

Step 15: The Logout Action

In the logout action, we can just assign the session to Nil, and redirect the user to the login page.

                          def logout                                  session[:user_id] = nil                                  redirect_to :action => 'login'                          end                    

Step 16: Routes Configuration

Finally, before you can test your code, edit your routes file, as follows:

                           root :to => 'sessions#login                            match "signup", :to => "users#new"                            match "login", :to => "sessions#login"                            match "logout", :to => "sessions#logout"                            match "home", :to => "sessions#home"                            match "profile", :to => "sessions#profile"                            match "setting", :to => "sessions#setting"                    

Final Thoughts

You’ve just learned how to implement a custom authentication system in your app from scratch. Please keep in mind that there are more advanced libraries, which handle these functionalities for any Rails developer; they add Models, Migrations, Controllers, and Views to your application to handle the authentication for you.

Let’s lastly review the most common libraries:

  • Devise: Devise is the most common library in Rails for authentication. It has a 24.874 popularity rating, according to the ruby-toolbox website. It’s a flexible authentication solution for Rails, based on Warden.

  • Authlogic introduces a new type of model, which you can have as many as you want, and name them whatever you wish – just as with any other models. It’s clean, simple, and occupies the second place after Devise, with a popularity rating of 12.815.

  • OmniAuth is a standardized multi-provider authenticator for web applications. Powerful and flexible, it has a popularity rating of 11.128

There are additional libraries for handling authentication, which you can review here.


Conclusion

In this tutorial, we covered the entire process of implementing a simple user authentication system in your application. You should now have an overall understanding of the techniques and methods required to build your own, should you choose to do so.

Thanks for reading, and please let me know if you have any questions in the comments below!



No hay comentarios:

Publicar un comentario