After this lesson, students will be able to:
- Describe why encryption is important for sensitive data
- Explain how has_secure_password works
- Create a user model with has_secure_password & the appropriate attributes
- Save a user with an encrypted password
- Find a user by email & password
Before this lesson, students should already be able to:
- Build a rails app from scratch
- Create RESTful routes with corresponding actions in Rails controllers
- Create relationships between models in Rails
- Use partials and templates in views
Today, we are going to learn about making our site more secure. Authentication is about making sure you know the identity of the person accessing your site and the data you store. Essentially, it's about asking for passwords, or other proof of identity. It doesn't guarantee anything - if your girlfriend knows your email password, they could "pretend" to be you on a website.
Authentication should be used whenever you want to know exactly who is using or viewing your site. To know which user is currently logged-in, a website needs to store sensitive data - this data will, therefore, be encrypted.
When we talk about passwords, the commonly used word is "encryption", although the way passwords are used, most of the time, is a technique called "hashing". Hashing and Encryption are pretty similar in terms of the processes executed, but the main difference is that hashing is a one-way encryption, meaning that it's very difficult for someone with access to the raw data to reverse it.
Hashing | Symmetric Encryption - | |
---|---|---|
One-way function | Reversible Operation | |
Invertible Operation? | No, For modern hashing algorithms it is not easy to reverse the hash value to obtain the original input value | Yes, Symmetric encryption is designed to allow anyone with access to the encryption key to decrypt and obtain the original input value |
Now, we'll see how to implement hashing in a Ruby/Rails app.
We will be using a gem called bcrypt:
$ gem install bcrypt
$ ruby -e "require 'bcrypt'; pwd = gets; puts BCrypt::Password.create(pwd.chomp)"
rails new password_example
cd password_example
subl .
To implement hashing in our app, we will use bcrypt
as well :
gem 'bcrypt', '~> 3.1.2'
bundle
Now we can create a model called User
:
rails g model User email password_digest
rake db:migrate
The field password_digest
will be used to store the "hashed password", so the original password will never be stored. The logic for hashing a password the right way would be quite long to implement manually, so instead, we will just add a method provided by bcrypt-ruby
to enable all the hashing/storing the hash logic, and we will add a validation for the email:
In app/models/user.rb
:
class User < ActiveRecord::Base
has_secure_password
validates :email, presence: true, uniqueness: true
end
Now that we added this method has_secure_password
to the user model, we can use two "virtual" attributes on the model, password
and password_confirmation
. has_secure_password
gives you:
- password hashing and salting
- by the way, salting is when random data is used as additional input to a one-way function that hashes a password or passphrase
- authenticating against the hashed password
- password confirmation validation
Now, in a rails console
:
user = User.new
user.password = "password"
user.password_confirmation = "password"
user.email = "[email protected]"
user.save
user.password_digest
The long string of characters returned when we call the method user.password_digest
is the hashed password!
Now that you've seen me do it, let's try it together. First, create a rails app:
rails new password_example
cd password_example
subl .
We will first need to install the bcrypt
encryption gem. In our Gemfile
, we add the line:
gem 'bcrypt', '~> 3.1.2'
Then in terminal:
bundle
Now we can generate a User model with an email and pasword_digest
.
rails g model User email password_digest
rake db:migrate
In models/user.rb
:
class User < ActiveRecord::Base
has_secure_password
validates :email, presence: true, uniqueness: true
end
...and generate a users controller:
rails g controller users index new create
In controllers/users_controller.rb
:
class UsersController < ApplicationController
def index
@users = User.all
end
def new
@user = User.new
end
def create
@user = User.new user_params
if @user.save
redirect_to users_path
else
render 'new'
end
end
private
def user_params
params.require(:user).permit( :email, :password, :password_confirmation)
end
end
Note: Please note the use of Strong Params here
Let’s update our routes to include our users.
In config/routes.rb
:
resources :users, only: [:new, :index, :create]
Then we update our views:
In views/users/index.html.erb
:
<h1> Users index </h1>
<% @users.each do |user|%>
<%= user.email %>
<%= user.password_digest %>
<% end %>
In views/users/new.html.erb
:
<h1>Sign Up</h1>
<%= form_for @user do |f| %>
<% if @user.errors.any? %>
<div class="error_messages">
<h2>Form is invalid</h2>
<ul>
<% for message in @user.errors.full_messages %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
In views/application/layout.html.erb
, at the top of our body section, add our flash messages:
<% flash.each do |name, message| %>
<div class="flash-message flash-message-<%= name %>">
<%= message %>
</div>
<% end %>
Now to allow the user to login and out, we will need to create a sessions_controller
:
rails g controller sessions new create destroy
Now we can create routes for this controller. In configs/routes.rb
you should now have:
root "users#index"
resources :users, only: [:new, :index, :create]
get 'login', to: 'sessions#new'
resources :sessions, only: [:new, :create, :destroy]
In sessions_controller.rb
we'll need to add some logic to handle the user's input for email and password:
class SessionsController < ApplicationController
def new
end
# Authentication logic
def create
user = User.find_by_email(params[:email])
# email_found && params[:password] == hashed_password ?
if user && user.authenticate(params[:password])
redirect_to root_path, notice: "logged in!"
else
flash.now.alert = "invalid login credentials"
render "new" # sessions#new
end
end
def destroy
redirect_to root_url, notice: "logged out!"
end
end
Note: Flash.now vs Flash
flash.now[:message] = "Hello current action"
When you need to pass an object to the next action, you use the standard flash assign ([]=). When you need to pass an object to the current action, you use now, and your object will vanish when the current action is done.
Now we will need to add a login form:
In "views/sessions/new.html.erb":
<h1>Login</h1>
<%= form_tag sessions_path do %>
<div class="field">
<%= label_tag :email %>
<%= text_field_tag :email %>
</div>
<div class="field">
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div class="actions"><%= submit_tag "Log in" %></div>
<% end %>
Now we can delete the extra templates:
- sessions/create.html.erb
- sessions/destroy.html.erb
- users/create.html.erb
After all this, create a user via http://localhost:3000/users/new
. Then, checkout http://localhost:3000/users
- you should see the email you've used and the hashed password.
Now, you can try to sign-in by going to http://localhost:3000/sessions/new
, and if you enter the credentials that matched your created user, you should see a message on the next page "Logged in!"; otherwise, the else
statement will have executed, and you'll see "invalid login credentials".
...and that is how to implement an authentication system in Rails!
We've covered a lot! You now know what happens when a password is saved in a database and how to authenticate a user. At the moment, our Rails app does not "remember" that a user is authenticated, to implement this, we will need functions, which is the topic of the next lesson.
- Describe the difference between hashing and encrypting.
- Explain what
has_secure_password
does for your application.