#Auth in Rails
This workshop is important because:
Authenticating users is key to authorizing who is allows to do what in an application. We need to impliment these concepts in order to create and experience where users can have differentiated experiences on the application. For example, when we go to Feedly, we'd like to see new stories customized for each of our profiles, and when others go to gmail, we'd like them prevented from accessing our emails.
After this workshop, developers will be able to:
- Implement an authentication system in Rails that securely stores users' passwords
- Build routes, controllers, and views necessary for a user to signup & login
Before this workshop, developers should already be able to:
- Illustrate the request/response cycle
- Compare and contrast sessions & cookies
- Build an MVC Rails application
- Authentication verifies that a user is who they say they are. When a user logs into our site, we authenticate them by checking that the password they typed in matches the password we have stored for them.
- Authorization is the process of determining whether or not a user has permission to to perform certain actions on our site. For example, a user may be authorized to view their profile page and edit their own blog posts, but not to edit another user's blog posts.
A user must always first be authenticated, then it can be determined what they are authorized to do.
Example: When Sarah enters a bar, a bouncer looks at her photo ID to ensure (authenticate) that she is who she claims. Sarah is thirty years old so she is allowed (authorized) to drink.
##Password Hashing
You've already seen how not to store a password.
In order to authenticate a user, we need to store their password in our database. This allows us to check that the user typed in the correct password when logging into our site.
The downside is that if anyone ever got access to our database, they would also have access to all of our users' login information. We use a hashing algorithm to avoid storing plain-text passwords in the database. We also use salt to randomize the hashing algorithm, providing extra security against potential attacks. The plain-text password that has been hashed can be referred to as the password digest.
Think of a digested password as a firework. It is very easy to explode a firework (hash plaintext into a digest), but next to impossible to reverse that process (turn the digest back into plaintext). If I wanted to see if two sets of fireworks are the same (a user is logging in, aka has provided their password and wishes to be authenticated) we have to explode the fireworks again to compare it with the original explosion (take the provided plaintext password, hash it again using the same algorithm, and match it with the saved password digest).
Let's start a new Rails application:
rails new rails-auth -T -B -d postgresql
. (This command sets up a new application with no tests, no automatic bundle, and postgres as the database.)cd rails-auth
rake db:create
subl .
The library of choice for password hashing is BCrypt
, which we will add to our gemfile. In Rails, the convention is to add all our business logic into the models, so we will be writing most of our code in the User
model.
Remember, remember: never store plaintext passwords, only the digested versions.
Let's uncomment bcrypt
at the bottom of our Gemfile
as we will need it to digest (hash) the plain-text password and store it in a password_digest
field of our database's users
table.
Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
Then run bundle
to install bcrypt
and the other gems.
As soon as something is installed via bundler we can access it via our rails console.
Let's play in console.
Loading development environment (Rails 4.1.6)
## Let's create our first password & save the hashed output to a variable
2.1.0 :001 > hashed_pass = BCrypt::Password.create("swordfish")
=> "$2a$10$6MQQCxBpfu16koDVs3zkbeSXn1z4fqKx9xLp4.UOBQBDkgFaukWM2"
## Let's compare our password to another
2.1.0 :003 > BCrypt::Password.new(hashed_pass) == "tunafish"
=> false
## Let's compare our password to original
2.1.0 :004 > BCrypt::Password.new(hashed_pass) == "swordfish"
=> true
## Exit
2.1.0 :005 > exit
Note: the
==
method forBCrypt::Password
is different than the typical comparator in Ruby say for anObject
.
BCrypt::Password.instance_method(:==) == String.instance_method(:==)
=> false
How will Bcypts ==
help us authenticate a User
?
BCrypt uses "salt" to protect against rainbow table attacks and is an adaptive function (see section: "Adaptive Hash Functions") to protect against brute-force attacks.
- Add the rspec gem to both test and development environments, then run
bundle
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'pry-rails'
# Rspec allows us to write tests for rails
gem 'rspec-rails'
end
bundle
and then run the command rails g rspec:install
to initialize rspec as your testing suite. Now a spec
directory has been created. Additionally, rspec will automatically generate tests for any files created by the rails generate
command.
Let's leave our controllers be for the time being and setup our models. Our tests depend on a User
model existing. The default attribute type is string, if we don't specify.
rails g model user email password_digest
email
is the natural username for our user, and the password_digest
is where we'll store the user's hashed password.
If you ever make a mistake during a generation, you can reverse it with
rails destroy <resourceType> <resourceName>
. In this case it would simply berails d model user
.
Great, let's run the migrations!
rake db:migrate
Now we can ensure we build our User
model to specifications by passing some tests we've been given!
Inside the directory spec
overwrite the existing file /models/user_spec.rb
with the below tests.
require "rails_helper"
describe User, type: :model do
it "can create a new user" do
expect(User.new).to be_a User
end
context 'Initialization' do
subject(:user) { User.new }
it "allows the getting of a password" do
expect(user).to respond_to(:password)
end
it "allows the setting of a password" do
expect(user).to respond_to(:password=).with(1).argument
end
it "creates a password digest when a password has been set" do
#password digest starts as nil
expect(user.password_digest).to be_nil
#password is set
user.password = "swordfish"
#password digest is created after password is set
expect(user.password_digest).not_to be_nil
end
it "ensures the password digest is not the password" do
user.password = "swordfish"
expect(user.password_digest).not_to eq(user.password)
end
end
context 'Validation' do
subject(:user) do
#create a user in active memory
User.new({
email: "bana@na.com",
password: "adsf1234",
password_confirmation: "adsf1234"
})
end
it "validates presence of password_digest" do
#clear values of password & password_confirmation
user.password_digest = nil
expect(user).not_to be_valid
end
it "validates presence of email" do
#clear values of email
user.email = nil
expect(user).not_to be_valid
end
it "validates password & password confirmation match" do
user.password_confirmation = "not the same"
expect(user).not_to be_valid
end
end
context 'Authentication' do
before(:all) do
#clear all users
User.destroy_all
#save a user to the database
@user = User.create({
email: "shmee@me.com",
password: "jumanji",
password_confirmation: "jumanji"
})
end
it "restricts passwords from saving to the db" do
found_user = User.all.first
expect(found_user.password).to eq(nil)
end
describe "#authenticate" do
it "returns the user when the correct password is provided" do
expect(@user.authenticate("jumanji")).to eq(@user)
end
it "returns false when an incorrect password is provided" do
expect(@user.authenticate("ijnamuj")).to eq(false)
end
end
describe "::confirm" do
it "checks if a specified user & password combination exists" do
user_email = "shmee@me.com"
user_password = "jumanji"
found_user = User.find_by_email(user_email)
expect(User.confirm(user_email, user_password)).to eq(found_user.authenticate(user_password))
end
end
end
end
Run them with:
rspec
In the process of passing these tests we will build all the logic for an authentication system! You should never have to write this code from scratch, but it is very important you understand what is going on.
Exercise: Think, pair, share on what the below code is doing (7 minutes).
class User < ActiveRecord::Base
BCrypt::Engine.cost = 12
# email & password_digest fields must exist
validates :email, :password_digest, presence: true
# a user must have a password & password confirmation field
# the fields are match against each other but never persisted to the database
validates_confirmation_of :password
# TODO: add validator for unique emails
def password=(unencrypted_password)
#raise scope of password to instance
@password = unencrypted_password
self.password_digest = BCrypt::Password.create(@password)
end
def password
#get password, equivalent to `attr_reader :password`
@password
end
# to authenticate the user using bcrypt's built in
def authenticate(unencrypted_password)
# check that a hashed version of the unencrypted password is the same as the secure password
BCrypt::Password.new(self.password_digest) == unencrypted_password ? self : false
end
# class method `::confirm`
def self.confirm(email_param, password_param)
# add a unique email validator later
user = User.find_by_email(email_param)
user.authenticate(password_param)
end
end
###Challenge: Unique emails
Write a test that validates the uniquness of emails, watch it fail, then write the code that passes it.
###User stories...
A user should be able to...
-
go to
/signup
and have the application execute theusers#new
action to render/views/users/new.html.erb
. -
see a
form_for
onusers#new
that displays email, password, and password_confirmation data. -
submit the form on
users#new
tousers#create
which creates a new user, logs them in, and redirects touser#show
. -
go to
/users/:id/
and see their profile page.
Let's get started!
Let's edit our config/routes.rb
file...
Rails.application.routes.draw do
root to: "welcome#index"
get "/login", to: "sessions#new"
post "/sessions", to: "sessions#create"
get "/sign_up", to: "users#new", as: "sign_up"
resources :users
end
If you haven't seen it before, resources
will auto generate all the RESTful routes for a User
.
Run rake routes
to see all the application's routes.
Challenge: Start your application and pass these user stories. On the root_path
:
- A user can see a welcome message`
- A user can click a "Sign Up" button that directs them to the
sign_up_path
-
Let's create
UsersController
with the command:rails g controller users new create show
-
Let's add a private method that creates strong parameters for specific attributes of the user
You should end up something along the lines of...
class UsersController < ApplicationController
def new
end
def create
# TODO: once the controller is implemented don't forget to also sign the user in
end
def show
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
##Challenge: Implement Signup
####Step 1
- For your
/sign_up
route, which hits the actionusers#new
, render a filenew.html.erb
in/views/users
.
####Step 2
- In that view add a
form_for
referencing user; have it post tousers#create
withemail
,password
andpassword_confirmation
.
####Step 3
- Create the user in
users#create
and when done have it redirect touser#show
(later we will have them also be logged-in in this step)- Tip: create a condition that checks if the user was saved correctly. Hint: first build the user in memory with
.new
then checkif @user.save
proceed as normalelse
render the signup page again.
- Tip: create a condition that checks if the user was saved correctly. Hint: first build the user in memory with
####Step 4
users#show
will find the current user and display their profile page
##Session Management
Since creating a session is essentially what we mean when want to login, and logging out is destroying a session. We have a single controller dedicated to session management, SessionsController
.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
#TODO: render a login view
end
def create
#call the User#confirm method
if User.confirm(params[:email], params[:password])
# this creates the session, logging in the user
session[:user_id] = user.id
#redirect to the show page
redirect_to user_path(user.id)
else
#there was an error logging the user in
redirect_to login_path
end
end
def destroy
#TODO: logout the current user
end
end
After we authenticate someone we set session[:user_id] = user.id
. This allows the user.id
to be stored in a cookie for lookup later.
Now that we know how to login a user with session[:user_id] = user.id
let's also make sure to do that when a user is signed up (it is good UX for a signup to automatically perform a login).
Tip: Try running rake notes
to see all the items that have been marked as TODO
in the comments.
Since we need to authenticate each request and to do so we have to read the user_id
out of the session
object, let's consider making a few helper methods to do so.
A login for a user is when we set a unique identifier for a user in their session, aka session[:user_id] = user.id
, so they are able to maintain their logged-in state by sending that unique piece of data back to us each time they send a new request. What is we could have a helper method that does this for us and caches the value of the current_user
for the duration of each request?
class ApplicationController < ActionController::Base
def current_user
@current_user ||= session[:user_id] && User.find_by_id(session[:user_id])
end
helper_method :current_user #make it available in views
end
Who can explain how the
current_user
method works?
The above method defines @current_user
if it is not already defined. The way the &&
operator works is that it will keep evaluating if session[:user_id]
is defined and then set @current_user
to whatever the last item evaluated is; in this case it would be User.find_by_id(session[:user_id])
, so the user itself.
Prefer
find_by_id(<id>)
instead offind(<id>)
, as it does not rails an exception, but instead returnsnil
if the id is invalid.
The method current_user
in is very useful for:
- Conditional views based on the
current_user
's state- I.e. is a login or logout button displayed in the nav_bar?
- Authorization to CRUD resources
- I.e. determine if
current_user
is accessing their own or another's information.
- I.e. determine if
In session#destroy
, delete the key :user_id
, clear the @current_user
, and redirect to your root_path
.
session.delete(:user_id)
@current_user = nil
###Refactor
- Using
has_secure_password
can magically refactor a lot of our password storing logic in the User model. Try it out and see if the tests still pass...
###Authorization
You can secure your routes with a :before_action
filter. This code can be run before any controller#action
in the application. For example let's say a user must be logged before they can see all the posts
in the application. You could create a private method in the application controller with a name such as require_login
.
application_controller.rb
class ApplicationController < ActionController::Base
private
def require_login
if !current_user
#send an http response to stop program execution
redirect_to root_path
end
end
end
Note: One could also use
unless
in lieu ofif !
Now use a before_action
to run the require_login
method before any actions the PostController
will perform.
posts_controller.rb
class PostController < ApplicationController
before_action :require_login
def index
end
end
Further define which actions this hook is applied to with the only
& except
options.
We want to notify users of any errors. Rails patterns use a flash hash to do so.
session_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user_params = params.require(:user)
user = User.confirm(user_params[:email], user_params[:password])
if user
# use our handy login method
login(user)
redirect_to user_path(user.id)
else
# Flash an error message
flash[:error] = "Failed To Authenticate. Please try again."
redirect_to "/login"
end
end
end
The twitter-bootstrap-rails gem has a bootstrap_flash
that adds some nice styling to the flash messages. Require the library in project with rails generate bootstrap:install static
We can then render these message and style them with a class that matches their name on all pages.
application.html.erb
<!-- include this just above the yield -->
<%= bootstrap_flash %>
<%= yield %>
Now you need to pass in flash messages to your views! For example my user controller could look like this.
user_controller.rb
class UsersController < ApplicationController
before_action :require_login, only: :index
# to illustrate a before_action
def index
@users = User.all
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
#login user
session[:user_id] = @user.id
#redirect to user#show w/ success message
redirect_to @user, flash: { success: "Successfully signed up!" }
else
#there was an error, go back to signup page & display message
redirect_to sign_up_path, flash: { error: @user.errors.full_messages.to_sentence }
end
end
def show
@user = current_user
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
##Closing Thoughts
- What is the difference between authorization and authentication?
- How should we not store passwords?
- Why is BCrypt useful and how do we use it to authenticate a user
- What does it mean for a user to be "logged in"?
##Additional Resources