Authentication

Ruby on Rails Intermediate/Advanced — 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.