Lesson 1 — Rails 8 authentication generator
Why not Devise?
Devise is the dominant Rails authentication solution and it’s excellent for what it does. But it generates a large amount of code you don’t own — views in engine paths, controllers you can’t easily inspect, a DSL that abstracts away what’s actually happening.
Rails 8 ships with a built-in authentication generator that takes the opposite approach: it generates readable, ownable Ruby code directly into your app. You can read every line, understand what it does, and change anything. It’s less feature-rich than Devise out of the box, but it teaches you what authentication actually involves.
For a tutorial series, ownable code wins.
Before you run the generator
The Rails 8 authentication generator assumes no User model exists.
We have one — with existing associations, validations, seed data,
fixtures, and tests. Running the generator naively causes conflicts.
Work through this section carefully before running anything.
Step 1 — Run the generator, decline all overwrites
|
|
When prompted to overwrite any existing file, answer n (no) to
all of them. The generator still creates all the new files it needs —
Session model, Authentication concern, SessionsController,
PasswordsController, Current model, and mailers — without touching
your existing User model, fixtures, or tests.
Step 2 — Fix the migration before running it
The generator creates two migrations — create_users and create_sessions. The create_sessions migration is fine. The create_users migration must not be run as-is — it will fail because the users table already exists.
Rename and edit it:
|
|
Rename the file from TIMESTAMP_create_users.rb to TIMESTAMP_add_password_digest_to_users.rb, then open it and replace the entire content with:
|
|
Now migrate:
|
|
This adds only the password_digest column that has_secure_password needs, without touching the existing users table structure.
And this is exactly what should go in the GitHub issue as the required workaround — it’s more precise than just “delete the migration.”
Step 3 — Add generator additions to User manually
Open app/models/user.rb and add the lines the generator would have
added:
|
|
has_secure_password adds a password virtual attribute and
authenticate_by(email_address:, password:) for login. It requires the
password_digest column the migration just created.
generates_token_for :password_reset creates a signed, expiring token
for password reset links — no separate token column needed.
has_many :sessions lets Rails manage one session record per login,
allowing users to sign out of specific devices in future.
Step 4 — Handle email vs email_address
The Rails 8 generator uses email_address throughout — the
Authentication concern, SessionsController, and session forms all
reference email_address. Our User model has an email column.
Rather than renaming the column (which requires a migration and updating all references throughout the app), add an alias:
|
|
alias_attribute makes email_address a full alias for email —
reads, writes, validations, and form helpers all work against either
name. The generator’s code works unchanged. Your existing code works
unchanged. The database column stays email.
Step 5 — Update the fixture
The generator would have added password_digest to the users fixture.
Add it manually to test/fixtures/users.yml:
|
|
BCrypt::Password.create generates a real bcrypt hash. Fixtures with
has_secure_password need this so authenticate_by works correctly
in tests.
Step 6 — Update user tests
Add authentication-related tests to test/models/user_test.rb:
|
|
Step 7 — Verify the setup
Run the user tests before proceeding:
|
|
All tests should pass. If anything fails here, fix it before moving on — the rest of the module depends on auth being correctly wired.
What the generator created
With the manual additions in place, here’s a summary of what now exists:
app/models/session.rb — a Session model that stores one record
per active login. Each session has a user_agent and ip_address for
security auditing.
app/models/current.rb — a CurrentAttributes store for
request-scoped data:
|
|
CurrentAttributes is thread-safe — Current.session is set at the
start of each request and cleared at the end.
app/concerns/authentication.rb — the core auth concern included
in ApplicationController:
|
|
require_authentication runs before every action. It tries to resume
an existing session from the signed cookie — if none is found, it
redirects to the sign in page.
start_new_session_for(user) creates a Session record, stores its
id in a signed httponly cookie, and sets Current.session. The
httponly flag prevents JavaScript from reading the cookie —
an important security measure.
app/controllers/sessions_controller.rb — handles sign in and
sign out. app/controllers/passwords_controller.rb — handles
password reset.
Both already have allow_unauthenticated_access so they’re accessible
without being logged in.
Remove the current_user stub
Now that Authentication provides a real current_user, remove the
stub from ApplicationController:
|
|
The stub def current_user = User.first is gone. current_user now
comes from the Authentication concern via Current.session&.user.
before_action :require_authentication now runs on every request.
Unauthenticated users are redirected to the sign in page — which means
the sign in and sign up pages themselves must be accessible without
auth. We handle that in the next lesson.