This tutorial is for adding authentication to a Ruby on Rails 5 app using Bcrypt and has_secure_password. It is based on a Rails 4 version using Ryan Bates's approach from Railscast #250 Authentication from Scratch (revised) and a gist by thebucknerlife (@thebucknerlife on Twitter).
For an example, see this sample generated by following this tutorial or for futher context of auth in a larger app, see this WDI lab solution implementing auth in Rails 4.
-
If you're starting a new app rather than adding auth to an existing app, generate a new rails app
rails new auth_sample -T -B -d postgresql
-
Create a user model with a
name
,email
andpassword_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 named
password_digest
and some kind of attribute to identify the user (like an email or a username). -
Run
rails db:create
andrails db:migrate
in the command line to migrate the database. -
Add the routes below to your
config/routes.rb
file.# config/routes.rb Rails.application.routes.draw do # These routes will be for signup. The first renders a form in the browser. 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
-
Create a users controller:
# app/controllers/users_controller.rb class UsersController < ApplicationController end
-
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
-
Now create the view file where we put the signup form.
<!-- app/views/users/new.html.erb --> <h1>Sign up!</h1> <%= form_for :user, url: '/users' do |f| %> Name: <%= f.text_field :name %><br> Email: <%= f.text_field :email %><br> Password: <%= f.password_field :password %><br> Password Confirmation: <%= f.password_field :password_confirmation %><br> <%= 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.
-
Add logic to create action and add the private
user_params
method to ensure we're only using intended input parameters from the form. You will need to adjust the parameters inside the.permit()
method based on how you set up yourUser
model - be sure to permit all the fields you expect to receive from your front end.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
-
Go to your Gemfile and uncomment the 'bcrypt' gem. We need bcrypt to securely store passwords in our database.
source 'https://rubygems.org' # ... # Use ActiveModel has_secure_password gem 'bcrypt' # ...
-
Go to the
User
model file and addhas_secure_password
. This is the line of code that gives ourUser
model authentication methods via bcrypt.# app/models/user.rb class User < ApplicationRecord has_secure_password end
See
has_secure_password
's documentation and source code. -
Run
bundle install
from the terminal, and then restart your rails server. Take a moment to visit the/signup
route and use the form. Make sure that you aren't seeing any errors on submission, but that it's directing you to the home page once you sign up. If it's successful, then we've created a user! -
Use the rails console to check whether the users we've created are visible in the DB.
-
Create a sessions controller. This is where we create and destroy sessions (aka log in / log out).
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create end def destroy end end
-
Create a form for users to log in 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 %>
-
Update your routes file to include new routes for the sessions controller.
GifVault::Application.routes.draw do # ... # 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
-
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
-
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
. Thehelper_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
-
Now you can add
:authorize
as abefore_action
to any controller that you want to secure. This will force user's to login before they can see the actions in this controller. For example, in the gif controller below the app will make sure that a user is logged in before taking either action. You would need to add these routes to the routes.rb in order for these to work.# app/controllers/gif_controller.rb class GifController < ApplicationController before_action :authorize def cool end def free end end
-
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>
You can also add a <% console %>
tag below the <%= yield %>
tag to test whether or not current_user
has the value that you expect.
- To get this to work, you'll need a new homepage. Make a simple view at
users/views/index.html.erb
and add the following to yourconfig/routes.rb
:
# config/routes.rb
root to: "users#index"
Also add an index method to your user controller to render that simple page.
# controllers/users_controller.rb
def index
end
You should be able to see login and logout options throughout your site!