Auth in Rails
Authentication Review
Authentication is the process of verifying a user's credentials to prove they are who they say they are. This is different than authorization, enabling or disabling access to specific resources.
To authenticate our users we typically ask them for a password we can associate to their email
. A password is a very private piece of information that must be kept secret, and so, we strategically obscure in such a way that one can only confirm a user is authentic and never uncover what their actual password.
Our library of choice for password obfuscation is called BCrypt
. This will be added to our gemfile for authentication setup later. In Rails, the convention is to push more logic into our models, so it shouldn't come as a surprise that authentication setup will happen in the user model.
Creating a Secure Password
We can "roll-our-own" authentication system with BCrypt by adding instance and class methods to our User
model. The methods below handle creating a user with a secure password and authenticating a user:
#
# app/models/user.rb
#
class User < ActiveRecord::Base
BCrypt::Engine.cost = 12
validates :email, :password_digest, presence: true
validates_confirmation_of :password
def authenticate(unencrypted_password)
secure_password = BCrypt::Password.new(self.password_digest)
if secure_password == unencrypted_password
self
else
false
end
end
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
def self.confirm(email_param, password_param)
user = User.find_by_email(email_param)
user.authenticate(password_param)
end
end
Rather than rolling our own auth, we can take a more concise, "rails-y" approach with has_secure_password
, which adds authentication methods for us.
Sessions
HTTP is a stateless protocol. Sessions make it stateful.
Most applications need to keep track of certain state of a particular user. This could be the contents of a shopping basket or the user id of the currently logged in user. Without the idea of sessions, the user would have to identify, and probably authenticate, on every request. Rails will create a new session automatically if a new user accesses the application. It will load an existing session if the user has already used the application.
A session usually consists of a hash of values and a session id, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session id. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method:
session[:user_id] = @current_user.id
User.find(session[:user_id])
RESTful routes for Auth
Even when we are doing authentication we need to be RESTful. For signing up we'll be doing users#create
and for logging in we'll call sessions#create
. We are going to manage logging in from a sessions_controller.rb
with :new
(login page), :create
(create a session), and :delete
(logout) actions.
#
# config/routes.rb
#
resources :users, only: [:new, :create, :destroy]
resources :sessions, only: [:new, :create, :destroy]
#
# app/controllers/sessions_controller.rb
#
class SessionsController < ApplicationController
def new
# Renders the sessions/new.html.erb template
end
def create
#pass in array that user_params returns as arguments using a splat
user = User.confirm(params[:email], params[:password])
if user
#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 new_session_path
end
end
def destroy
session[:user_id] = nil
redirect_to root_path
end
end
Current User
If you want an @current_user
method to be available in all your controllers and views, then you'll want to define an application level helper method that looks up the user from the session[:user_id]
if it exists.
#
# app/controllers/sessions_controller.rb
#
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
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
You can use this @current_user
object to do a lot
- Authorization -
@current_user.present?
means the user is logged in. - User Profile -
user_path(@current_user)
will be the path to the current user's profile