Admin Authentication

Website Rebuild with Ruby on Rails

Chapter 9 · Admin Authentication

Laravel's own Chapter 9 found a clean, zero-configuration win: its default hasher already matched the legacy site's bcrypt hash exactly. Rails' has_secure_password also defaults to bcrypt — but the honest version of this chapter isn't a repeat of that same clean finding. There's a real, worth-verifying nuance underneath it.

has_secure_password

# Gemfile gem "bcrypt", "~> 3.1.7"
# app/models/admin_user.rb class AdminUser < ApplicationRecord has_secure_password end

has_secure_password adds a virtual password/password_confirmation pair and an authenticate(password) method to the model, backed by a real password_digest column — expected to already hold the legacy site's existing bcrypt hash for its own admin row.

A Real, Verified Nuance: $2a$ vs. $2y$

Not assumed automatically seamless
Ruby's bcrypt gem, when it generates a new hash, tags it with the $2a$ version prefix. PHP's password_hash() tags its own bcrypt output $2y$ — a distinct prefix PHP introduced to mark a historical fix for how certain implementations handled 8-bit characters in a password. In practice, modern bcrypt implementations — including Ruby's own bcrypt gem — treat $2a$, $2b$, and $2y$ as interchangeable for verification, and an ordinary alphanumeric admin password will authenticate correctly either way. But this is a real point worth checking deliberately when a hash crosses from one language's ecosystem into another — not something to assume automatically seamless the way Laravel's same-ecosystem match was.

Normalizing the Hash on Login

if admin&.authenticate(params[:password]) admin.update(password: params[:password]) # regenerates password_digest, tagged $2a$ session[:admin_user_id] = admin.id end

On a successful login, re-assigning password triggers has_secure_password's own setter, regenerating password_digest using Ruby's own bcrypt gem — tagged $2a$ going forward. This quietly normalizes the legacy PHP-generated hash to a Rails-native one over time, the same spirit as Django Rebuild 9's own automatic hash-upgrade-on-login, closing the version-tag question naturally rather than needing a permanent resolution.

Closing Chapter 8's Gap

# app/controllers/pages_controller.rb class PagesController < ApplicationController before_action :require_admin, only: [:update_title] private def require_admin redirect_to new_session_path, alert: "Please log in." unless session[:admin_user_id] end end

before_action :require_admin is exactly the missing piece Chapter 8 flagged — a genuinely separate concern from Strong Parameters, added the way Rails' own idiom expects: as a callback, not a change to page_params itself.

Password Hashing, Four Frameworks

Next.jsDjangoLaravelRails
Default algorithmNone — hand-rolledPBKDF2BcryptBcrypt
Matches the legacy hash?Only via manual codeNo — needed reconfigurationYes — zero configurationYes, functionally — with a real version-tag nuance worth verifying
Session modelSigned JWT cookieServer-side sessionServer-side sessionServer-side session

Hands-On Exercises

Exercise 1

Explain what has_secure_password provides, and name the exact database column it expects to already exist.

📄 View solution
Exercise 2

Explain the real, verified difference between the $2a$ and $2y$ bcrypt version tags, and explain why this chapter treats Rails' own bcrypt match differently from Laravel's clean zero-configuration finding.

📄 View solution
Exercise 3

Explain what before_action :require_admin does, and explain exactly which gap from Chapter 8 it closes.

📄 View solution

Chapter 9 Quick Reference

  • has_secure_password — adds authenticate, backed by a password_digest column
  • Verified nuance — Ruby's bcrypt gem tags new hashes $2a$, PHP tags them $2y$; functionally compatible, but a real cross-ecosystem detail worth checking
  • Hash normalization on login — re-assigning password quietly upgrades the legacy hash to Rails' own tag over time
  • before_action :require_admin — closes Chapter 8's own missing-filter gap
  • Next chapter: Admin CRUD Interface