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 toNil
.
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 thenew
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