Rails 8 Authentication Generator — Built-in Auth Without Devise
Authentication is one of those problems that sounds simple and isn’t. Devise has solved it for Rails developers for over a decade — but it’s also a dependency that ships with opinions, a DSL, and abstractions that can make understanding your own auth code difficult. Rails 8 ships with a built-in authentication generator that produces plain, readable Ruby code you own from day one. No gem to learn, no DSL to fight, just your models and controllers.
What the Generator Produces
The Rails 8 auth generator doesn’t add a gem or install external code — it generates files directly into your app. What you get is yours: a User model, a Session model, controllers for registration and login, a Current concern, and a base controller with authentication helpers.
Setup:
rails generate authentication
This single command creates:
app/models/user.rb
app/models/session.rb
app/models/current.rb
app/controllers/sessions_controller.rb
app/controllers/passwords_controller.rb
app/controllers/concerns/authentication.rb
app/views/sessions/new.html.erb
app/views/passwords/new.html.erb
app/views/passwords/edit.html.erb
db/migrate/YYYYMMDDHHMMSS_create_users.rb
db/migrate/YYYYMMDDHHMMSS_create_sessions.rb
rails db:migrate
Done. Your app has working authentication.
The Generated Models
The generator produces clean, readable model code. No magic modules, no DSL — just Ruby.
Example:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: -> e { e.strip.downcase }
end
Example:
# app/models/session.rb
class Session < ApplicationRecord
belongs_to :user
end
has_secure_password (built into Rails via ActiveModel) handles password hashing via bcrypt, adds password and password_confirmation virtual attributes, and provides authenticate for verification. The Session model stores login sessions separately from users — each login creates a new session row, making it straightforward to list active sessions or revoke access.
The sessions migration looks like:
Example:
class CreateSessions < ActiveRecord::Migration[8.0]
def change
create_table :sessions do |t|
t.references :user, null: false, foreign_key: true
t.string :ip_address
t.string :user_agent
t.timestamps
end
end
end
The Authentication Concern
The generator creates an Authentication concern that ApplicationController includes. This is where session management lives.
Example:
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id])
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_url
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
The cookie is signed (tamper-resistant), HTTP-only (not accessible via JavaScript), and uses SameSite: Lax (CSRF protection). The session ID stored in the cookie maps to a database row — revocation is a row deletion.
Skipping Authentication for Public Actions
By default, all actions require authentication. To make specific actions public, use allow_unauthenticated_access.
Example:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication
end
Example:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
allow_unauthenticated_access only: [:index, :show]
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
def new
# Requires authentication — redirects to login if not signed in
@post = Post.new
end
end
Accessing the Current User
The Current class (thread-safe via ActiveSupport::CurrentAttributes) provides the current user throughout the request.
Example:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end
Example:
# In any controller
def create
@post = Current.user.posts.build(post_params)
# ...
end
# In any view
<%= Current.user.email_address %>
# In any model or service object
class PostPublisher
def publish(post)
post.update!(
published: true,
published_by: Current.user
)
end
end
Current.user is available anywhere during the request cycle — controllers, views, models, service objects — without needing to pass it explicitly.
Password Reset Flow
The generator includes a password reset flow via email token.
Example:
# app/controllers/passwords_controller.rb (generated)
class PasswordsController < ApplicationController
allow_unauthenticated_access
def new; end
def create
if (user = User.find_by(email_address: params[:email_address]))
PasswordsMailer.reset(user).deliver_later
end
# Always redirect regardless — don't leak whether email exists
redirect_to new_session_url, notice: "Check your email for reset instructions."
end
def edit
@user = User.find_signed!(params[:token], purpose: :password_reset)
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_session_url, alert: "Password reset link is invalid or has expired."
end
def update
@user = User.find_signed!(params[:token], purpose: :password_reset)
@user.update!(params.permit(:password, :password_confirmation))
redirect_to new_session_url, notice: "Password updated. Please sign in."
end
end
Signed tokens are generated with User#signed_id(purpose:, expires_in:) — Rails’ built-in ActiveRecord::SignedId. No random token column needed in the database.
Pro-Tip: After generating auth, audit what you actually need and delete what you don’t. If your app uses OAuth only (GitHub login, Google login), you may not need the passwords controller at all. The generator’s value is not that it produces a perfect final solution — it’s that it produces readable, idiomatic Rails code that you can read, understand, and modify without fighting a third-party DSL. Read every generated file before shipping to production. Knowing exactly what your authentication code does is not optional.
Conclusion
Rails 8’s authentication generator is a meaningful shift in how Rails approaches auth. Rather than hiding implementation behind a dependency, it puts plain Ruby code in your app that you can read, understand, and own. It covers the common cases — login, logout, sessions, password reset — with secure defaults (signed cookies, bcrypt, signed tokens). For applications that need OAuth or more complex access control, it’s a foundation to extend, not a cage to work around.
FAQs
Q1: Should I use the Rails 8 auth generator or Devise?
It depends on your needs. Devise offers more out of the box — OAuth, confirmable, lockable, token auth. The generator is better if you want to understand and control your auth code, or if Devise’s feature set is overkill. For simple email/password auth on Rails 8, the generator is the cleaner starting point.
Q2: Is has_secure_password production-ready?
Yes. It uses bcrypt with a configurable cost factor (default: 12). BCrypt is battle-tested for password hashing. has_secure_password has been in Rails since version 3.1.
Q3: How do I add OAuth login alongside the generated auth?
Add OmniAuth to your Gemfile, configure the provider, and create a separate authentication flow that finds or creates a user by provider UID. The generated auth code doesn’t interfere — they can coexist.
Q4: How do I implement “remember me” functionality?
The generated cookie is already permanent (20-year expiry). For session-only cookies, change cookies.signed.permanent to cookies.signed in the start_new_session_for method.
Q5: Can I use this generator on Rails 7?
The generator was introduced with Rails 8. For Rails 7, you can manually replicate the pattern or use the authentication-zero gem, which generates similar code and supports Rails 7.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀