Authentication
๐ Authentication
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."
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)
| Concern | Express (typical JWT) | Rails (default sessions) |
|---|---|---|
| Where identity lives | Inside the signed token itself | Server-trusted reference in an encrypted cookie |
| Server storage needed? | No (stateless) | No, by default (cookie store) โ but can be database-backed |
| Instant revocation | Hard โ needs a separate blocklist | Easy โ invalidate server-side |
| Route protection | Auth middleware (express2-1) | before_action :require_login |
| Password hashing | bcrypt, called manually | bcrypt, 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.
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.
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.
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.