2016-06-10

Rails user authentification from scratch

This post is for rails beginners. I want to help you get a better understanding of user authentification in rails.

Let’s begin with authentification vs authorization.

Authentification
- Who is the user?
- Is the user really he/she pretends to be?
- => Username + password

Authorization
- What is the user allowed to do?
- Has the user permission to access resource X?
- => Different rules for different user roles like admin/guest/…

Many libraries/gems to choose from…

Indeed, there are quite a lot of libraries out there who help you with user authentification and authorization. But in order to really understand how authentification works, it’s a good idea to write the code from scratch at least once. This is what this post is all about. At the end I will introduce you to some useful libraries for the future.

I assume you know the basics about ruby and rails and won’t explain every single line of code. But feel free to ask upcoming questions.

Step 0 - Create a new rails app

rails new authentification_from_scratch --database=postgresql

Step 1 - Create a user model

rails generate model user name email password_digest

We will use a gem called “bicrypt” to encrypt our passwords. The Gem requires our user model to have an attribute called “password_digest”. Watch out for typos!

Step 2 - Setup the database

rake db:create
rake db:migrate

Step 3 - Setup some initial routes

<-- config/routes.erb -->  
Rails.application.routes.draw do

  #root url => First page after login
  root to: 'users#show'
  
  #Url for signup page => posts form parameters to users#create
  get '/signup' => 'users#new'

  #Form from users#new will post to users#create
  post '/users' => 'users#create'

end

Step 4 - Create users controller

rails generate controller users

Step 5 - Add actions for the previously created routes

<-- app.controllers/users_controller.erb -->

class UsersController < ApplicationController

  def show
  end

  def new
  end

  def create
  end
end

### Step 6 - Create the signup form

<-- app/views/users/new.html.erb -->

<h1>Signup here!</h1>
  <%= form_for :user, :url: '/users' do |f| %>
    Name: <%= f.text_field :name %>
    Email <%= f.text_field :email %>
    Password: <%= f.password_field :password %>
    Password Confirmation: <%= f.password_field :password_confirmation %>
    <%= f.submit "Submit" %>
  <% end %>

The password_confirmation field is not required, so you can ommit it if you like. Keep in mind all the rails conventions for the form builder. Our form will send its parameters wrapped in a hash table called “user” to the “/users” url using the post method. We have already defined this route in routes.rb

Step 7 - Receive the form parameters => create action

<-- app/controllers/users_controller.rb -->

class UsersController < ApplicationController

  def show
  end

  def new
  end

  def create
    user = User.new(user_params)
    if user.save
      session[:user_id] = user.id
      redirect_to '/'' # after signup redirect to root page
    else
      redirect_to '/signup' #after failed signup => redirect to signup page again
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end

First we need to whitelist our user parameters. Then we can use them to actually create a new user in the database. After the user is saved succesfully, we will create a new session cookie (cookie = little piece of information stored in your browser) and store the user_id in it. Rails offers two types of cookies: session cookies and normal cookies. The only difference between them is the way they are stored. Session cookies are stored encrypted and normal cookies are stored in plain text. You can adjust the code, signup and see the difference in your browser stored cookies.

<-- Change cookie type -->

  def create
    user = User.new(user_params)
    if user.save
      cookie[:user_id] = user.id
      redirect_to `/`
    else
      redirect_to '/signup'
    end
  end

Step 8 - Install bycrypt gem

Usally the bycrypt gem is just commented out. So uncomment or add this line:

<-- Use ActiveModel has_secure_password -->
  gem 'bcrypt', '~> 3.1.7'

After you have added the line install the gem with

bundle install

Step 9 - User Model (where the magic happens)

I want to cover the core principles of authentification in this post. That’s why I won’t add any constraints like checking for duplicated email addresses or a required password length.

Here we just need to add

<-- app/models/user.rb -->

class User < ActiveRecord::Base

  has_secure_password

end

What has happend here? The method “has_secure_password” compares our user.password and user.password_confirmation, creates a hash of our password and stores it inside the attribute password_digest (Step 1).
If you even want to do that on your own, take a look at this post on stackoverflow.

Step 10 - Create the sessions controller

<-- app/controllers/sessions_controller.rb -->

class SessionsController < ApplicationController

  def new #login form
  end

  def create #login action
  end

  def destroy #logout action
  end

end

Step 11 - Create the login form

<-- app/views/sessions/new.html.erb -->

<h1>Login</h1>

<%= form_tag '/login' do %>
  Email: <%= text_field_tag :email %>
  Password: <%= password_field_tag :password %>
  <%= submit_tag "Submit" %>
<% end %>

Step 12 - Add the new login/logout routes

<-- config/routes.rb -->
Rails.application.routes.draw do

  root to: 'users#new'

  get '/login' => 'sessions#new' #send login form
  post '/login' => 'sessions#create' #receive login form
  get '/logout' => 'sessions#destroy' #logout

  get '/signup' => 'users#new'
  post '/users' => 'users#create'

end

Step 13 - Add the login and logout logic

<-- app/controllers/sessions_controller.rb -->

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by_email(params[:email])
    #If user exists AND password is entered correctly
    if user && user.authenticate(params[:password])
      #Store user id in a brwoser cookie
      #This is how we will keep the user logged in while he can navigate on our site
      session[:user_id] = user.id
      redirect_to '/'
    else
      #Login failed => back to login form
      redirect_to '/login'
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to '/login'
  end

end

Step 14 - Check if user is logged in

<-- app/controllers/application_controller.rb -->

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  # returns current_user if it already exists or looks if there is a session_cookie with the correct value and assigns current_user afterwards
  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
  helper_method :current_user #to make the method available in our views

  def authorize
    redirect_to '/login' unless current_user
  end

end

I have also included three lines of authorization here. Otherwise there is no sense in loggin in.

Step 15 - Setup our top secret page

rails g controller Pages secret 
<-- app/views/pages/secret.html.erb -->
<h1>Top Secret</h1>

<h2>Hallo <%= @current_user.name %></h2>

Update the route.rb

Rails.application.routes.draw do

  root to: 'pages#secret'

  get '/secret' => 'pages#secret'

  get '/login' => 'sessions#new'
  post '/login' => 'sessions#create'
  get '/logout' => 'sessions#destroy'


  get '/signup' => 'users#new'
  post '/users' => 'users#create'

end

No we can determine who will be allowed to access our top secret page.

Step 16 - Authorize all succeessfully logged in users to see the page

<-- app/controllers/pages_controller.rb -->

class PagesController < ApplicationController
  before_filter :authorize

  def secret
  end

end

Before our server will execute the secret action (and send the top secret site to our client’s browser) the authorize method is called. As we have seen in step 13, the authorize method checks if the user is logged in, if not it will redirect the user to the login page.

What’s missing?

Authentification in action (demo)

demo

Gems vs code from scratch

It is definitely helpful to knwo how to implement user authentification by only using bycrypt or even completly from scratch. However it is quite boring and insecure to do it again and again for every single app. Therefore it’s better to use a gem. Now it’s up to you, if you want to maintain your own authentification gem or if you want other people to do it for you.

Here is a short list of interesting gems:
- device (authentification)
- clearance (authentification)
- cancancan (authorication)
- pundit (authorication)

comments powered by Disqus