Authentication

Ruby on Rails Intermediate/Advanced
Course 2 ยท Chapter 1 ยท Authentication

๐Ÿ” Authentication

Course 1 built CRUD for posts and comments, but never checked who was actually making a request. This chapter adds real login โ€” password hashing via has_secure_password, and Rails' default session-based approach, which works fundamentally differently from the JWT-based authentication Express Course 2 covered.

๐Ÿ”‘ has_secure_password

$ rails generate migration CreateUsers email:string password_digest:string
$ rails db:migrate
# Gemfile -- uncomment this line (it's included but commented out by default)
gem "bcrypt", "~> 3.1.7"

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
end

has_secure_password is a single macro that adds a huge amount of functionality on top of the password_digest column: virtual password and password_confirmation attributes (never actually stored), automatic bcrypt hashing before save, and an authenticate(password) method that returns the user if the password matches, false otherwise.

user = User.create(email: "philip@example.com", password: "secret123", password_confirmation: "secret123")
user.password_digest    # => "$2a$12$K3JR9..." -- the bcrypt hash, this is what's actually stored

user.authenticate("wrong")       # => false
user.authenticate("secret123")  # => the user object itself

๐Ÿช SessionsController โ€” Login as a Singular Resource

# config/routes.rb
resource :session, only: [:new, :create, :destroy]   # singular "resource" -- no :id, there's only ever one current session

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:email])
    if @user&.authenticate(params[:password])
      session[:user_id] = @user.id
      redirect_to root_path
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path
  end
end

Notice @user&.authenticate(...) โ€” the safe navigation operator from Ruby Course 2, Chapter 8, guarding against a nil user (no matching email) before calling authenticate on it. resource :session (singular, no s) is Rails' convention for a resource where there's only ever one โ€” a session either exists for the current browser or it doesn't, so there's no :id in its routes.

โš–๏ธ Sessions vs JWT โ€” A Genuinely Different Architecture

Recall Express Course 2's authentication chapter: a JWT is generated on login, signed, and handed to the client, which then attaches it to every subsequent request โ€” the server verifies the signature and never needs to store anything about "who's logged in." Rails' default session[:user_id] = user.id works the opposite way:

JWT (Express) โ€” stateless

The token itself contains the user's identity, cryptographically signed. The server verifies the signature on each request but stores nothing between requests โ€” "statelessness" is the whole point, which is why JWTs scale easily across multiple servers with no shared session store.

Session cookie (Rails default) โ€” stateful

session[:user_id] is stored in an encrypted, signed cookie sent to the browser โ€” by default Rails keeps the session data itself inside that cookie (ActionDispatch::Session::CookieStore), tamper-proof via a secret key, but still fundamentally tied to "the browser holds a reference the server trusts."

โš  Revocation is the real practical difference

This is the tradeoff the completed Authentication & Session Security course already covered in the abstract, now concrete: a Rails session can be invalidated instantly and server-side โ€” change the session secret, or (with a database-backed session store) simply delete the session record, and that user is logged out immediately. A JWT, once issued, remains valid until it expires no matter what the server does, unless you build a separate revocation/blocklist mechanism โ€” the "statelessness" that makes JWTs easy to scale is the exact same property that makes them hard to revoke early.

๐Ÿ‘ค current_user and Protecting Routes

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
  helper_method :current_user   # makes it available in views too, not just controllers

  def require_login
    unless current_user
      redirect_to new_session_path, alert: "Please log in first"
    end
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :require_login, except: [:index, :show]   # guests can browse, must log in to write
  # ...
end

current_user uses the ||= memoization pattern from both Ruby courses โ€” computed once per request, cached in @current_user for any further calls in the same request. before_action :require_login is Rails' controller-scoped equivalent of Express's authentication middleware, running before the specified actions and redirecting away if the check fails โ€” a genuinely close conceptual match between the two frameworks, unlike most of this course's other comparisons.

๐Ÿ“œ Authentication, Side by Side

Express (JWT) vs Rails (Session)

ConcernExpress (typical JWT)Rails (default sessions)
Where identity livesInside the signed token itselfServer-trusted reference in an encrypted cookie
Server storage needed?No (stateless)No, by default (cookie store) โ€” but can be database-backed
Instant revocationHard โ€” needs a separate blocklistEasy โ€” invalidate server-side
Route protectionAuth middleware (express2-1)before_action :require_login
Password hashingbcrypt, called manuallybcrypt, via has_secure_password

๐Ÿ’ป Coding Challenges

Challenge 1: A User Model

Write the migration command and resulting migration file for a User with email and password_digest, then write the User model with has_secure_password and a uniqueness validation on email.

Goal: Set up the full has_secure_password foundation from scratch.

โ†’ Solution

Challenge 2: SessionsController

Write the full SessionsController with new, create, and destroy actions, following this chapter's pattern โ€” including the safe-navigation authenticate check and the correct session key assignment on login/logout.

Goal: Practice the complete login/logout flow end to end.

โ†’ Solution

Challenge 3: Protecting a Controller

Write current_user and require_login in ApplicationController, then add before_action :require_login to a CommentsController so that only index and show are accessible without logging in.

Goal: Practice guarding specific controller actions the way express2-1's middleware guarded specific Express routes.

โ†’ Solution

๐Ÿ’Ž Neither Approach Is Simply "Better"

JWTs win when an API needs to scale across many stateless servers with no shared session store, or serve non-browser clients (mobile apps, other services) where cookies don't naturally fit. Rails' session-based default wins for traditional server-rendered apps like the one this course has been building โ€” simpler to revoke, and it doesn't require the client to manage token storage or attach headers manually. Rails can do JWT-based API authentication too (relevant once Chapter 5 covers building APIs with Rails) โ€” the session default is a starting point suited to this course's app, not a hard limitation of the framework.

๐ŸŽฏ What's Next

Next chapter: Authorization โ€” now that Rails knows who is logged in, controlling what they're allowed to do, using Pundit or CanCanCan for role-based access.