Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple Authentication in Rail 4 Using Bcrypt

#Simple Authentication with Bcrypt

This tutorial is for adding authentication to a vanilla Ruby on Rails app using Bcrypt and has_secure_password.

The steps below are based on Ryan Bates's approach from Railscast #250 Authentication from Scratch (revised).

You can see the final source code here: repo. I began with a stock rails app using rails new gif_vault

##Steps

  1. Create a user model with a name, email and password_digest (all strings) by entering the following command into the command line: rails generate model user name email password_digest.

    Note: If you already have a user model or you're going to use a different model for authentication, that model must have an attribute names password_digest and some kind of attribute to identify the user (like an email or a username).

  2. Run rake db:migrate in the command line to migrate the database.

  3. Add these routes below to your routes.rb file. Notice I also deleted all the comments inside that file. Don't forget to leave the trailing end, though.

    # config/routes.rb
    
    GifVault::Application.routes.draw do
        
        # This route sends requests to our naked url to the *cool* action in the *gif* controller.
        root to: 'gif#cool'
        
        # I've created a gif controller so I have a page I can secure later. 
        # This is optional (as is the root to: above).
        get '/cool' => 'gif#cool'
        get '/sweet' => 'gif#sweet'
    
        # These routes will be for signup. The first renders a form in the browse, the second will 
        # receive the form and create a user in our database using the data given to us by the user.
        get '/signup' => 'users#new'
        post '/users' => 'users#create'
    
    end
  4. Create a users controller:

    # app/controllers/users_controller.rb
    
    class UsersController < ApplicationController
    
    end
  5. Add a new action (for rendering the signup form) and a create action (for receiving the form and creating a user with the form's parameters.):

    # app/controllers/users_controller.rb
    
    class UsersController < ApplicationController
    
        def new
        end
    
        def create
        end   
    
    end
  6. Now create the view file where we put the signup form.

    <!-- app/views/users/new.html.erb -->
    
    <h1>Signup!</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 %>

    A note on Rail's conventions: This view file is for the new action of the users controller. As a result, we save the file here: /app/views/users/new.html.erb. The file is called new.html.erb and it is saved inside the views folder, in a folder we created called users.

    That's the convention: view files are inside a folder with the same name as the controller and are named for the action they render.

  7. Add logic to create action and add the private user_params method to sanitize the input from the form (this is a new Rails 4 thing and it's required). You might need to adjust the parameters inside the .permit() method based on how you setup your User model.

class UsersController < ApplicationController

def new
end

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

private

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

end ```

  1. Go to your Gemfile and uncomment the 'bcrypt' gem. We need bcrypt to securely store passwords in our database.

    source 'https://rubygems.org'
    
    # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
    gem 'rails', '4.0.4'
    
    # Use sqlite3 as the database for Active Record
    gem 'sqlite3'
    
    ...
    
    # Use ActiveModel has_secure_password
    gem 'bcrypt', '~> 3.1.7'
    
    ...
    
  2. Go to the User model file and add has_secure_password. This is the line of code that gives our User model authentication methods via bcrypt.

    # app/models/user.rb
        
    class User < ActiveRecord::Base
    
      has_secure_password
    
    end
  3. Run bundle install from the terminal then restart your rails server.

    Note: Windows users might have issues with bcrypt. If so, copy the error into Google and look for answers on Stack Overflow. There is documentation online for how to fix Windows so the bcrypt works.

  4. Create a sessions controller. This is where we create (aka login) and destroy (aka logout) sessions.

    # app/controllers/sessions_controller.rb
    
    class SessionsController < ApplicationController
    
      def new
      end
    
      def create
      end
    
      def destroy
      end
    
    end
  5. Create a form for user's to login with.

    <!-- 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 %>
  6. Update your routes file to include new routes for the sessions controller.

    GifVault::Application.routes.draw do
    
      root to: 'gif#cool'
    
      # these routes are for showing users a login form, logging them in, and logging them out.
      get '/login' => 'sessions#new'
      post '/login' => 'sessions#create'
      get '/logout' => 'sessions#destroy'
    
      get '/signup' => 'users#new'
      post '/users' => 'users#create'
      
    end
  7. Update the sessions_controller with the logic to log users in and out.

      # app/controllers/sessions_controller.rb
    
      def create
        user = User.find_by_email(params[:email])
        # If the user exists AND the password entered is correct.
        if user && user.authenticate(params[:password])
          # Save the user id inside the browser cookie. This is how we keep the user 
          # logged in when they navigate around our website.
          session[:user_id] = user.id
          redirect_to '/'
        else
        # If user's login doesn't work, send them back to the login form.
          redirect_to '/login'
        end
      end
    
      def destroy
        session[:user_id] = nil
        redirect_to '/login'
      end
  8. Update the application controller with new methods to look up the user, if they're logged in, and save their user object to a variable called @current_user. The helper_method line below current_user allows us to use @current_user in our view files. Authorize is for sending someone to the login page if they aren't logged in - this is how we keep certain pages our site secure... user's have to login before seeing them.

    # 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
    
      def current_user
        @current_user ||= User.find(session[:user_id]) if session[:user_id]
      end
      helper_method :current_user
    
      def authorize
        redirect_to '/login' unless current_user
      end
    
    end
  9. Add a before_filter to any controller that you want to secure. This will force user's to login before they can see the actions in this controller. I've created a gif controller below which I'm going to secure. The routes for this controller were added to the routes.rb in the beginning of this tutorial.

    # app/controllers/gif_controller.rb
    
    class GifController < ApplicationController
    
      before_filter :authorize
    
      def cool
      end
    
      def free
      end
    
    end
  10. You can update your application layout file to show the user's name if they're logged in and some contextual links.

    <!-- app/views/layout/application.html.erb -->
    
    <!DOCTYPE html>
    <html>
    <head>
      <title>GifVault</title>
      <%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
      <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
      <%= csrf_meta_tags %>
    </head>
    <body>
    
    # added these lines.
    <% if current_user %>
      Signed in as <%= current_user.name %> | <%= link_to "Logout", '/logout' %>
    <% else %>
      <%= link_to 'Login', '/login' %> | <%= link_to 'Signup', '/signup' %>
    <% end %>
    
    <%= yield %>
    
    </body>
    </html>

##Things Missing

  • Adding flash messages would be simple and provide feedback to the user if things go wrong.

-- All done! Feel free to fork and update this. Reach me at @thebucknerlife on Twitter.

@jdxcode

This comment has been minimized.

Copy link

jdxcode commented Apr 8, 2014

instead of

current_user ||= User.find(session[:user_id]) if session[:user_id]

you want to do

@current_user ||= User.find(session[:user_id]) if session[:user_id]

if it is in a local variable it won't be saved

@thebucknerlife

This comment has been minimized.

Copy link
Owner Author

thebucknerlife commented Apr 8, 2014

Done. Thanks @dickeyxxx

@gxespino

This comment has been minimized.

Copy link

gxespino commented Oct 27, 2014

Thanks a lot for this!

@BrunoCanongia

This comment has been minimized.

Copy link

BrunoCanongia commented Nov 4, 2014

So simple! Thank you.

@drymar

This comment has been minimized.

Copy link

drymar commented Nov 16, 2014

Thanks man!

@Johnsalzarulo

This comment has been minimized.

Copy link

Johnsalzarulo commented Dec 9, 2014

This guide is gold. I spent hours playing with devise for authentication. This is dead simple. Thanks!

@jd-gray

This comment has been minimized.

Copy link

jd-gray commented Jan 8, 2015

Thank you for the help! I was wondering how to accomplish that before_filter :authorize

@lilijreey

This comment has been minimized.

Copy link

lilijreey commented Jan 19, 2015

Thanks for your help! clear talk.

@oakfield

This comment has been minimized.

Copy link

oakfield commented Jan 19, 2015

Thanks, I found this very helpful.

@myerspa

This comment has been minimized.

Copy link

myerspa commented Feb 5, 2015

Little bit late since this has been around since last April...but this is awesome! Straight forward implementation of a concept that pretty much every app will need.

Nice!

@efecarranza

This comment has been minimized.

Copy link

efecarranza commented Feb 12, 2015

awesome job!
what i never understood was why we needed password_digest, thanks!

@riliwanrabo

This comment has been minimized.

Copy link

riliwanrabo commented Feb 27, 2015

Awesome Guide!!!

@EricFries

This comment has been minimized.

Copy link

EricFries commented Mar 14, 2015

Great guide. Thanks!

@augustoody

This comment has been minimized.

Copy link

augustoody commented Mar 31, 2015

Thanks for the help, awesome job!

@ashok4

This comment has been minimized.

Copy link

ashok4 commented Apr 8, 2015

Great guide. but please mention where to put validation&error messages
can you please help me...!

@Epigene

This comment has been minimized.

Copy link

Epigene commented Apr 24, 2015

Excellent writeup, though one could wish the author had used rails route helpers instead of path literals.

@prwhitehead

This comment has been minimized.

Copy link

prwhitehead commented May 4, 2015

Hello,

Firstly thank you very much for this, its been really useful in getting tro grips with Rails Auth.

If I may, I would like to make one suggestion. In your UserController the create method redirects back to the new method; meaning all validation information is lost. It took me a while to figure out what was going on - so for others that are new to the paradigms laid out by Rails, the following will rerender the form and display your error messages for the signup form:

  def create
    @user = User.new(user_params)

    if @user.save
      session[:user_id] = @user.id
      redirect_to :home, notice: 'Account created successfully'
    else
      flash[:error] = 'An error occured!'
      render 'new'
    end
  end
@lamh85

This comment has been minimized.

Copy link

lamh85 commented May 19, 2015

Hello all

First, thank you very much for this guide. It is very invaluable for people who want to learn about authentication.

Just a caution about #14. Perhaps I coded something differently, but the step does not work because my params hash is different:

{"utf8"=>"✓", "authenticity_token"=>"i4mIRjS+H3/V2y5LZ8/H+lSymYOotuDW16BDWKemxDMHAyEMrC2VklQoyyQZx0868AQvJnsr+ZsMGxUjiLuq5Q==", 
"login"=>{"email"=>"mike@smith.com", "password"=>"[FILTERED]"}, "commit"=>"Log in"}

In other words, my hash does not have a key called "email", as mentioned in step 14. Calling it would require params[:login][:email] instead of just params[:email]. The same is for calling the password.

Just a heads up in case #14 doesn't work for you.

Thanks again for this guide!

@kevlarr

This comment has been minimized.

Copy link

kevlarr commented Jul 2, 2015

This guide was just phenomenal.

@iMikie

This comment has been minimized.

Copy link

iMikie commented Jul 9, 2015

Boy did this ever fill an incredible need. I wish everyone would write as clearly as you.
Thank you so much.

@ijunaid8989

This comment has been minimized.

Copy link

ijunaid8989 commented Aug 8, 2015

where is that method Authenticate?

@Corstiaan84

This comment has been minimized.

Copy link

Corstiaan84 commented Aug 11, 2015

Great primer on basic Rails auth. Thanks!

@iscott

This comment has been minimized.

Copy link

iscott commented Aug 26, 2015

This is awesome! Thanks for posting.

@sabrams86

This comment has been minimized.

Copy link

sabrams86 commented Aug 28, 2015

Awesome! I'm just getting into rails and this is perfect for introducing me to everything needed to use has_secure_password

@edenasevic1

This comment has been minimized.

Copy link

edenasevic1 commented Sep 9, 2015

First of all great guide, I really enjoyed reading it. But, I think there is one mistake.

We generated User model with 3 attributes, name, email and password_digest, but in user_params we take all 4 values from form fields and try to save it in database, which causes error, because we have one parameter more than we need.

@thomascarterx

This comment has been minimized.

Copy link

thomascarterx commented Sep 15, 2015

This really is something everyone really needs. I too wrestled around with devise and cannot believe how easy and fast it was to do it like this. Great tutorial. Thanks so much for putting this on here. It really helped a lot. You should consider doing more tutorials, if you haven't already, and you have the time. Your communication style is very efficient.

@lstanard

This comment has been minimized.

Copy link

lstanard commented Oct 15, 2015

Thank you! This is an incredibly handy reference!

@sadanandkenganal

This comment has been minimized.

Copy link

sadanandkenganal commented Oct 18, 2015

Its very simple and nice. Please explain validation also for signp and login form.

@andim27

This comment has been minimized.

Copy link

andim27 commented Nov 9, 2015

Super solution for windows users:
Note: Windows users might have issues with bcrypt. If so, copy the error into Google and look for answers on Stack Overflow.
This gem doesn't work on win 7 64 bit/ruby 2.2 rails 4.2/
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Please fix problem for windows users !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

@lskd

This comment has been minimized.

Copy link

lskd commented Nov 10, 2015

Awesome walk thru, thanks for sharing.

@cloudsben

This comment has been minimized.

Copy link

cloudsben commented Jan 16, 2016

Awesome, really simple authentication

@juancpgo

This comment has been minimized.

Copy link

juancpgo commented Feb 4, 2016

Great! Thanks. What about password resetting (when user looses it)? Does anybody suggest an approach?

@AndrewHendrie

This comment has been minimized.

Copy link

AndrewHendrie commented Feb 9, 2016

Typo in the first code block:

These routes will be for signup. The first renders a form in the browse, the second will

receive the form and create a user in our database using the data given to us by the user.

browse should be browser

@oklaiss

This comment has been minimized.

Copy link

oklaiss commented Mar 21, 2016

Very helpful, thanks for this.

@shahfaizanali

This comment has been minimized.

Copy link

shahfaizanali commented Mar 24, 2016

How to disable password requirement. I have Employee < User. I don't need authentication for employees.

@cdekker

This comment has been minimized.

Copy link

cdekker commented Mar 26, 2016

@shahfaizanali best practice is to make a bare virtual class 'VirtualUser' and have 2 classes inherit from that:

  • Employee < VirtualUser
  • User < VirtualUser

Then you can implement the authentication stuff from this guide only in the User class and not be bothered by it in the Employee class.

However, all employees will have the unused Password_digest column, which is somewhat of a design flaw. Maybe rethink your model structure? :)

@ManojDatt

This comment has been minimized.

Copy link

ManojDatt commented Mar 26, 2016

These steps has really worked for me..Thanks for help..

@khalidh64

This comment has been minimized.

Copy link

khalidh64 commented Mar 31, 2016

wow really superb....
thnx.... keep it up bro...
it is very very useful to beginners...

@andrewdotcudzilo

This comment has been minimized.

Copy link

andrewdotcudzilo commented Apr 27, 2016

Excellent bare-metals guide

@SeptivianaSavitri

This comment has been minimized.

Copy link

SeptivianaSavitri commented Jun 27, 2016

wow, its amazingly simple,thanks for the tutorial.
But I wonder if we can use this method to combine with omniauth?
Thanks :)

@FottyM

This comment has been minimized.

Copy link

FottyM commented Jul 2, 2016

awesome

@oquidave

This comment has been minimized.

Copy link

oquidave commented Jul 29, 2016

Worked fine for me. Thanks. Although I would to know what goes on behind the scenes.

@Yuvisiva12

This comment has been minimized.

Copy link

Yuvisiva12 commented Aug 5, 2016

Good Tutorial. Thanks for this..

@silverdr

This comment has been minimized.

Copy link

silverdr commented Aug 10, 2016

One of the unfortunately scarce pieces of good documentation for Rails. TNX.

@jamesaduke

This comment has been minimized.

Copy link

jamesaduke commented Aug 10, 2016

really great tutorial

@martinbeentjes

This comment has been minimized.

Copy link

martinbeentjes commented Aug 23, 2016

Great straight-to-the-point tutorial! Thanks for this!

@cloudsben

This comment has been minimized.

Copy link

cloudsben commented Sep 7, 2016

Great Tutorial !

@aaossa

This comment has been minimized.

Copy link

aaossa commented Sep 20, 2016

If you're on Windows, using Ruby 2.3 and bcrypt gives you problems, this comment was the solution for me 👌

@levanlinh1995

This comment has been minimized.

Copy link

levanlinh1995 commented Oct 2, 2016

tks, nice

@i8igmac

This comment has been minimized.

Copy link

i8igmac commented Nov 6, 2016

this is frustrating... everything looks good but it dont work...

after creating a user account, i expect to see a new page like '/home'

welcome to a new page, you are logged in

@ziomio

This comment has been minimized.

Copy link

ziomio commented Dec 13, 2016

awesome, thank you!

@RayedB

This comment has been minimized.

Copy link

RayedB commented Dec 20, 2016

This tutorial is the real deal! Thanks !

@samguergen

This comment has been minimized.

Copy link

samguergen commented Dec 29, 2016

You are the man. Thank you!

@9mm

This comment has been minimized.

Copy link

9mm commented Jan 17, 2017

You should use session.delete(:user_id) instead.

session[:user_id] = nil will leave the :user_id key in the session hash, this will destroy the key and value, as if your session never had any value assigned to that key.

http://stackoverflow.com/a/3995975/794481

@ancaciascaiu

This comment has been minimized.

Copy link

ancaciascaiu commented Feb 17, 2017

Hi, I find this guide really useful, but I discovered a mistake that can trigger misunderstanding: The Note in Part 1 says "that model must have an attribute names password_digest". The correct way to say this is that you need to leave the model attribute as "password" and make sure you have a migration attribute of "password_digest". This way it'll work. :)

@MauricioRibeiroA

This comment has been minimized.

Copy link

MauricioRibeiroA commented Mar 21, 2017

Perfect explanation!

@export-mike

This comment has been minimized.

Copy link

export-mike commented Apr 3, 2017

you might want to reconsider your session ids to be unique for a given session and not the user id.

@caronalex06

This comment has been minimized.

Copy link

caronalex06 commented May 19, 2017

Perfect and simple. Wow ! Thanks.

@ip-ilamparithi

This comment has been minimized.

Copy link

ip-ilamparithi commented Aug 8, 2017

how to set a persistent cookie using bcrypt?

@vamuigua

This comment has been minimized.

Copy link

vamuigua commented Aug 9, 2017

Thanks...Bcrypt is cool!

@BertZZ

This comment has been minimized.

Copy link

BertZZ commented Sep 16, 2017

Does this still all work on rails 5

@jattoabdul

This comment has been minimized.

Copy link

jattoabdul commented Nov 29, 2017

thanks alot for this man

@ajcubeta

This comment has been minimized.

Copy link

ajcubeta commented Dec 20, 2017

Thanks man, this really helps. Moving Devise to BCrypt =)

@lreb

This comment has been minimized.

Copy link

lreb commented Jan 23, 2018

thank you

@stevecondylios

This comment has been minimized.

Copy link

stevecondylios commented Feb 17, 2018

It works on rails 5, just change

before_filter

to

before_action

@triton11

This comment has been minimized.

Copy link

triton11 commented Mar 9, 2018

Just wanted you to know its 2018 and this guide is still so helpful!

@dhoangk07

This comment has been minimized.

Copy link

dhoangk07 commented May 7, 2018

Thanks for posting, this one is very helpful.

@reemhosny

This comment has been minimized.

Copy link

reemhosny commented Jun 30, 2018

it also works on rails 5
Thanks a lot , great work

@stevecondylios

This comment has been minimized.

Copy link

stevecondylios commented Jul 13, 2018

I experienced a bug where a user couldn't immediately sign back in after signing out. This seemed to fix it:

In application.html.erb, replace
<%= link_to "Logout", '/logout' %>

With
<%= link_to "Logout", '/logout', data: { turbolinks: false } %>

@foundsatis

This comment has been minimized.

Copy link

foundsatis commented Jan 6, 2019

Still helpful in 2019!

Thanks for the guide. Exactly what I wanted.

Note:

Using before_filter :authorize, didn't work for me. I used before_action :authorize instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment