Rails 8 ships with a built-in authentication generator that creates a solid foundation for user login systems. This eliminates the need for Devise in many applications while providing full control over the authentication flow.
The Rails 8 Authentication Generator
Rails 8 introduced bin/rails generate authentication, which scaffolds a complete session-based authentication system. Unlike Devise, this generates plain Ruby code that lives in the application—readable, modifiable, and free of hidden magic.
Run the generator to create the foundation:
# Terminal
bin/rails generate authentication
bin/rails db:migrateThis creates several files: a User model with secure password handling, a Session model for tracking logins, controllers for registration and authentication, and the necessary views. The generated code uses Rails 8's has_secure_password with modern defaults.
Understanding the Generated User Model
The generator produces a User model with password handling and session management built in:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(email) { email.strip.downcase }
validates :email_address, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
# Generate a password reset token that expires
generates_token_for :password_reset, expires_in: 15.minutes do
password_salt&.last(10)
end
# Generate an email verification token
generates_token_for :email_verification, expires_in: 24.hours do
email_address
end
endThe generates_token_for method is a Rails 8 addition that creates secure, expiring tokens tied to model state. When the password changes (affecting password_salt), existing reset tokens automatically invalidate.
The Session Controller Pattern
Authentication flows through a SessionsController that creates and destroys sessions:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
allow_unauthenticated_access only: [:new, :create]
rate_limit to: 10, within: 3.minutes, only: :create,
with: -> { redirect_to new_session_path, alert: "Try again later." }
def new
end
def create
if user = User.authenticate_by(email_address: params[:email_address],
password: params[:password])
start_new_session_for(user)
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Invalid email or password."
end
end
def destroy
terminate_session
redirect_to new_session_path, notice: "Signed out."
end
private
def after_authentication_url
root_path
end
endNotice the built-in rate_limit directive—Rails 8 includes rate limiting without additional gems. The authenticate_by method performs constant-time comparison to prevent timing attacks.
The Authentication Concern
The generator creates a concern that handles session management across the application:
# app/controllers/concerns/authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?, :current_user
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session.present?
end
def current_user
Current.user
end
def require_authentication
resume_session || request_authentication
end
def resume_session
return if Current.session.present?
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]) if cookies.signed[:session_id]
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path, alert: "Please sign in."
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent,
ip_address: request.remote_ip).tap do |session_record|
set_current_session(session_record)
end
end
def set_current_session(session_record)
Current.session = session_record
cookies.signed.permanent[:session_id] = {
value: session_record.id,
httponly: true,
same_site: :lax
}
end
def terminate_session
Current.session&.destroy
cookies.delete(:session_id)
end
endThis concern uses the Current pattern to store the authenticated session and user. The allow_unauthenticated_access class method provides a clean API for controllers that need public access.
Adding Remember Me Functionality
Extend the generated code to support "remember me" tokens for persistent sessions:
# app/models/user.rb (add to existing model)
class User < ApplicationRecord
# ... existing code ...
generates_token_for :remember_me, expires_in: 2.weeks
end
# app/controllers/sessions_controller.rb (modify create action)
def create
if user = User.authenticate_by(email_address: params[:email_address],
password: params[:password])
start_new_session_for(user)
remember_user(user) if params[:remember_me] == "1"
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Invalid email or password."
end
end
private
def remember_user(user)
cookies.signed.permanent[:remember_token] = {
value: user.generate_token_for(:remember_me),
httponly: true,
same_site: :lax
}
end
# app/controllers/concerns/authentication.rb (add to resume_session)
def resume_session
return if Current.session.present?
if session_record = find_session_by_cookie
set_current_session(session_record)
elsif user = find_user_by_remember_token
start_new_session_for(user)
end
end
def find_user_by_remember_token
return unless cookies.signed[:remember_token]
User.find_by_token_for(:remember_me, cookies.signed[:remember_token])
endHandling Password Resets
The token generation already exists in the User model. Build the controller to use it:
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
allow_unauthenticated_access
rate_limit to: 5, within: 1.hour, only: :create
def new
end
def create
if user = User.find_by(email_address: params[:email_address])
PasswordMailer.reset(user).deliver_later
end
# Always show success to prevent email enumeration
redirect_to new_session_path, notice: "Check your email for reset instructions."
end
def edit
@user = User.find_by_token_for(:password_reset, params[:token])
redirect_to new_password_reset_path, alert: "Invalid or expired link." unless @user
end
def update
@user = User.find_by_token_for(:password_reset, params[:token])
if @user&.update(password_params)
@user.sessions.where.not(id: Current.session&.id).destroy_all
redirect_to new_session_path, notice: "Password updated. Please sign in."
else
render :edit, status: :unprocessable_entity
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
endThe find_by_token_for method returns nil for expired or invalid tokens, and the block in generates_token_for ensures tokens invalidate when the password changes.
Common Security Enhancements
Add session tracking to let users see and revoke active sessions:
# app/models/session.rb
class Session < ApplicationRecord
belongs_to :user
scope :active, -> { where("created_at > ?", 2.weeks.ago) }
scope :other_than, ->(session) { where.not(id: session.id) }
def current?(current_session)
id == current_session&.id
end
def device_name
agent = UserAgent.parse(user_agent)
"#{agent.browser} on #{agent.os}"
rescue
"Unknown device"
end
end
# app/controllers/sessions_controller.rb (add index and destroy_other)
def index
@sessions = current_user.sessions.active.order(created_at: :desc)
end
def destroy_all_others
current_user.sessions.other_than(Current.session).destroy_all
redirect_to sessions_path, notice: "Other sessions terminated."
endSummary
Rails 8 authentication provides a clean, understandable foundation that covers most application needs. The generated code handles secure password storage, session management, rate limiting, and token generation—all without external dependencies. Extend it with remember-me tokens, password resets, and session management as needed. For applications requiring OAuth or multi-factor authentication, consider adding targeted gems rather than replacing the entire system.