Rails 8 Authentication
Overview
Rails 8 includes a built-in authentication generator that creates a complete, secure authentication system without external gems.
Quick Start
Generate authentication
bin/rails generate authentication
Run migrations
bin/rails db:migrate
This creates:
-
User model with has_secure_password
-
Session model for secure sessions
-
Current model for request-local storage
-
Authentication concern for controllers
-
Session and Password controllers
-
Login/logout views
Generated Structure
app/ ├── models/ │ ├── user.rb # User with has_secure_password │ ├── session.rb # Session tracking │ └── current.rb # Current.user accessor ├── controllers/ │ ├── sessions_controller.rb # Login/logout │ ├── passwords_controller.rb # Password reset │ └── concerns/ │ └── authentication.rb # Auth helpers └── views/ ├── sessions/ │ └── new.html.erb # Login form └── passwords/ ├── new.html.erb # Forgot password └── edit.html.erb # Reset password
Core Components
User Model
app/models/user.rb
class User < ApplicationRecord has_secure_password has_many :sessions, dependent: :destroy
normalizes :email_address, with: -> { _1.strip.downcase }
validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } end
Session Model
app/models/session.rb
class Session < ApplicationRecord belongs_to :user
before_create { self.token = SecureRandom.urlsafe_base64(32) }
def self.find_by_token(token) find_by(token: token) if token.present? end end
Current Model
app/models/current.rb
class Current < ActiveSupport::CurrentAttributes attribute :session delegate :user, to: :session, allow_nil: true end
Authentication Concern
app/controllers/concerns/authentication.rb
module Authentication extend ActiveSupport::Concern
included do before_action :require_authentication helper_method :authenticated? end
class_methods do def allow_unauthenticated_access(**options) skip_before_action :require_authentication, **options end end
private
def authenticated? Current.session.present? end
def require_authentication resume_session || request_authentication end
def resume_session if session_token = cookies.signed[:session_token] if session = Session.find_by_token(session_token) Current.session = session end end end
def request_authentication redirect_to new_session_path end
def start_new_session_for(user) session = user.sessions.create! cookies.signed.permanent[:session_token] = { value: session.token, httponly: true } Current.session = session end
def terminate_session Current.session&.destroy cookies.delete(:session_token) end end
Usage Patterns
Protecting Controllers
class ApplicationController < ActionController::Base include Authentication
All actions require authentication by default
end
class PostsController < ApplicationController
All actions protected
end
class HomeController < ApplicationController
Allow public access to specific actions
allow_unauthenticated_access only: [:index, :about] end
Accessing Current User
In controllers
Current.user Current.user.email_address
In views
<%= Current.user.email_address %>
In models (use sparingly)
Current.user
Login Flow
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController allow_unauthenticated_access only: [:new, :create]
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 root_path, notice: "Signed in successfully" else flash.now[:alert] = "Invalid email or password" render :new, status: :unprocessable_entity end end
def destroy terminate_session redirect_to root_path, notice: "Signed out" end end
Testing Authentication
Request Specs
spec/requests/sessions_spec.rb
RSpec.describe "Sessions", type: :request do let(:user) { create(:user, password: "password123") }
describe "POST /session" do context "with valid credentials" do it "signs in the user" do post session_path, params: { email_address: user.email_address, password: "password123" }
expect(response).to redirect_to(root_path)
expect(cookies[:session_token]).to be_present
end
end
context "with invalid credentials" do
it "shows error" do
post session_path, params: {
email_address: user.email_address,
password: "wrong"
}
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe "DELETE /session" do it "signs out the user" do # First sign in post session_path, params: { email_address: user.email_address, password: "password123" }
delete session_path
expect(response).to redirect_to(root_path)
end
end end
Test Helper
spec/support/authentication_helpers.rb
module AuthenticationHelpers def sign_in(user) session = user.sessions.create! cookies[:session_token] = session.token end
def sign_out cookies.delete(:session_token) end end
RSpec.configure do |config| config.include AuthenticationHelpers, type: :request config.include AuthenticationHelpers, type: :system end
Protected Route Specs
spec/requests/posts_spec.rb
RSpec.describe "Posts", type: :request do let(:user) { create(:user) }
describe "GET /posts" do context "when not authenticated" do it "redirects to login" do get posts_path expect(response).to redirect_to(new_session_path) end end
context "when authenticated" do
before { sign_in(user) }
it "shows posts" do
get posts_path
expect(response).to have_http_status(:ok)
end
end
end end
References
-
See sessions.md for session management details
-
See current.md for Current attributes patterns
Common Customizations
Remember Me
def start_new_session_for(user, remember: false) session = user.sessions.create! cookie_options = { value: session.token, httponly: true } cookie_options[:expires] = 2.weeks.from_now if remember cookies.signed.permanent[:session_token] = cookie_options Current.session = session end
Multiple Sessions Tracking
In User model
def active_sessions sessions.where('created_at > ?', 30.days.ago) end
def terminate_all_sessions_except(current_session) sessions.where.not(id: current_session.id).destroy_all end
Rate Limiting
app/controllers/sessions_controller.rb
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Too many attempts" }