NYC
skills/dchuk/rails_ai_agents/authentication-flow

authentication-flow

SKILL.md

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 HomeController < ApplicationController
  allow_unauthenticated_access only: [:index, :about]
end

Accessing Current User

# In controllers and views
Current.user
Current.user.email_address

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

Test Helper

# test/test_helper.rb
class ActionDispatch::IntegrationTest
  def sign_in(user)
    session = user.sessions.create!
    cookies[:session_token] = session.token
  end

  def sign_out
    cookies.delete(:session_token)
  end
end

Session Controller Tests

# test/controllers/sessions_controller_test.rb
require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
  end

  test "GET new renders login form" do
    get new_session_path
    assert_response :success
  end

  test "POST create with valid credentials signs in user" do
    post session_path, params: {
      email_address: @user.email_address,
      password: "password123"
    }

    assert_redirected_to root_path
    assert cookies[:session_token].present?
  end

  test "POST create with invalid credentials shows error" do
    post session_path, params: {
      email_address: @user.email_address,
      password: "wrong"
    }

    assert_response :unprocessable_entity
  end

  test "DELETE destroy signs out user" do
    sign_in @user

    delete session_path

    assert_redirected_to root_path
    assert_nil cookies[:session_token]
  end
end

Protected Route Tests

# test/controllers/posts_controller_test.rb
require "test_helper"

class PostsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
  end

  test "redirects to login when not authenticated" do
    get posts_path
    assert_redirected_to new_session_path
  end

  test "shows posts when authenticated" do
    sign_in @user

    get posts_path
    assert_response :success
  end
end

References

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

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" }

Checklist

  • Authentication generator run
  • Test helper with sign_in/sign_out methods
  • Session controller tests written
  • Protected route tests written
  • Rate limiting on login
  • allow_unauthenticated_access on public pages
  • All tests GREEN
Weekly Installs
2
First Seen
7 days ago
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
windsurf2
codex2