Rails 8 Authentication: The Built-in Generator That Replaces Devise
Rails 8 ships with bin/rails generate authentication, a built-in authentication generator that creates session-based auth without any gems. If you’ve been reaching for Devise on every new project, this changes the calculus.
The generator produces a User model, a Session model, sign-in/sign-out controllers, password reset flow, and the Current object wiring — all in about 200 lines of code you own completely.
Running the Generator
rails new myapp
cd myapp
bin/rails generate authentication
bin/rails db:migrate
That’s it. You now have:
Usermodel withhas_secure_passwordand email normalizationSessionmodel tracking active sessions per userSessionsControllerfor sign-in and sign-outPasswordsControllerfor reset requestsAuthenticationconcern you include inApplicationController- Rate limiting on login attempts via
rate_limit
What the Generated Code Actually Does
The Authentication concern is the core. It provides require_authentication as a before action and exposes Current.user throughout your app:
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
end
private
def require_authentication
resume_session || request_authentication
end
def resume_session
if session_record = find_session_by_cookie
set_current_session(session_record)
end
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id])
end
def request_authentication
redirect_to new_session_path
end
def set_current_session(session_record)
Current.session = session_record
end
end
No magic. No 50-module deep stack trace when something breaks. You can read the entire auth flow in five minutes.
Session Management
Each sign-in creates a Session record tied to the user. The session token is stored in a signed cookie. This means you can list active sessions, revoke specific ones, and track login history — features that took custom work with Devise.
class SessionsController < ApplicationController
allow_unauthenticated_access only: [:new, :create]
rate_limit to: 10, within: 3.minutes, only: :create
def create
if user = User.authenticate_by(
email: params[:email],
password: params[:password]
)
start_new_session_for(user)
redirect_to root_path
else
redirect_to new_session_path,
alert: "Invalid email or password."
end
end
def destroy
terminate_session
redirect_to new_session_path
end
end
The rate_limit macro is new in Rails 8. It uses the request’s IP by default and returns HTTP 429 when exceeded. No Rack::Attack configuration needed for basic login throttling.
Password Resets
The generator creates a token-based password reset flow using generates_token_for:
class User < ApplicationRecord
has_secure_password
generates_token_for :password_reset, expires_in: 15.minutes do
password_salt&.last(10)
end
normalizes :email, with: ->(email) { email.strip.downcase }
end
generates_token_for was added in Rails 7.1. The token automatically invalidates when the password changes (because password_salt changes), and it expires after 15 minutes. No separate token column in the database. No cleanup cron jobs.
The PasswordsController handles the reset flow:
class PasswordsController < ApplicationController
allow_unauthenticated_access
def create
if user = User.find_by(email: params[:email])
PasswordMailer.reset(user).deliver_later
end
redirect_to new_session_path,
notice: "Check your email for reset instructions."
end
def update
if user = User.find_by_token_for(:password_reset, params[:token])
user.update!(password: params[:password])
redirect_to new_session_path,
notice: "Password updated. Please sign in."
else
redirect_to new_password_path,
alert: "Invalid or expired token."
end
end
end
Notice the constant-time response on create — it redirects with the same message whether the email exists or not. No user enumeration out of the box.
Customizing the Generated Code
Since you own all the code, customization is straightforward. Here are patterns I’ve used in production:
Adding Remember Me
# In SessionsController#create, after start_new_session_for(user):
if params[:remember_me] == "1"
cookies.signed.permanent[:session_id] = {
value: Current.session.id,
httponly: true,
same_site: :lax
}
end
Multi-device Session Management
The Session model makes this natural:
class SessionsController < ApplicationController
def index
@sessions = Current.user.sessions.order(created_at: :desc)
end
def destroy_other
Current.user.sessions.where.not(id: Current.session.id).destroy_all
redirect_to sessions_path, notice: "Other sessions revoked."
end
end
Adding User Registration
The generator deliberately omits registration. Add it yourself:
class RegistrationsController < ApplicationController
allow_unauthenticated_access
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
start_new_session_for(@user)
redirect_to root_path
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
When You Still Need Devise
The built-in generator covers the 80% case. You’ll want Devise (or another gem) when you need:
- OAuth/OmniAuth integration — social logins require more plumbing than the generator provides
- Confirmable emails — the generator has no email verification flow
- Lockable accounts — beyond rate limiting, if you need account lockout after N failures
- Complex role-based access — Devise + Pundit/CanCanCan is a well-tested stack
For API-only apps, you’ll also want to swap the cookie-based sessions for token auth. The generator’s session model gives you a good foundation, but you’ll need to add token generation and header-based authentication yourself.
Performance Considerations
Each authenticated request hits the sessions table to load the session record. On a busy app, add a database index (the migration already creates one on user_id) and consider caching:
def find_session_by_cookie
Rails.cache.fetch("session:#{cookies.signed[:session_id]}", expires_in: 5.minutes) do
Session.find_by(id: cookies.signed[:session_id])
end
end
Invalidate the cache on sign-out. For most apps under 10K concurrent users, the database lookup without caching is fine — it’s a primary key lookup.
Migration from Devise
If you’re moving an existing Devise app to built-in auth:
- Generate the authentication files
- Keep your existing
userstable — just ensurepassword_digestexists (Devise usesencrypted_password, so you’ll need a migration to rename or add the column) - Users must reset their passwords — Devise’s bcrypt format is compatible with
has_secure_password, but the column name differs - Replace
authenticate_user!withrequire_authenticationin your controllers - Swap
current_userreferences toCurrent.user - Move any Devise callback logic into your new controllers
This is a gradual process. Run both systems in parallel during migration by checking both authentication methods in your concern. I’ve done this migration on a Rails app with multi-tenancy and it took about two days for a medium-sized codebase.
Frequently Asked Questions
Does Rails 8 authentication work with API-only applications?
Not directly. The generator creates cookie-based session auth designed for server-rendered apps. For APIs, you’ll need to modify the Authentication concern to read tokens from the Authorization header instead of cookies. The Session model still works — just generate a token on creation and authenticate via header rather than signed cookie.
Can I use the Rails 8 auth generator with an existing User model?
Yes. The generator checks for an existing User model and skips creating a new one. It will add has_secure_password and the email normalization if missing. Run bin/rails generate authentication and review the generated migration — it only adds columns that don’t exist yet.
How does the built-in rate limiting compare to Rack::Attack?
The rate_limit macro uses Rails’ built-in cache store (usually Redis or Memcached in production) and limits by IP address. Rack::Attack offers more flexibility — throttling by user, custom discriminators, blocklists, and safelists. For login throttling, the built-in approach is sufficient. For comprehensive API rate limiting across multiple endpoints, Rack::Attack remains the better tool.
Is the generated authentication code secure enough for production?
Yes. It uses has_secure_password (bcrypt), signed cookies, constant-time token comparison, rate limiting, and prevents user enumeration on password reset. These are the same primitives Devise uses internally. The main security addition you should make is enforcing HTTPS (which Rails 8 does by default in production) and adding CSRF protection (also default). For zero-downtime deployments, the session table migration is straightforward.
Should I delete the generated code and write my own?
No. The generated code is meant to be modified, not replaced. Think of it as a well-tested starting point. Read through it, understand it, then adjust. The whole point is that you own the code — unlike Devise, where customization means monkey-patching gems or decorating controllers through a deeply nested inheritance chain.
About the Author
Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation