home / skills / thibautbaissac / rails_ai_agents / authentication-flow
/skills/authentication-flow
This skill helps you implement Rails 8 built-in authentication with a complete user, session, and password reset flow, including controllers and views.
npx playbooks add skill thibautbaissac/rails_ai_agents --skill authentication-flowReview the files below or copy the command above to add this skill to your agents.
---
name: authentication-flow
description: Implements authentication using Rails 8 built-in generator. Use when setting up user authentication, login/logout, session management, password reset flows, or securing controllers.
allowed-tools: Read, Write, Edit, Bash
---
# Rails 8 Authentication
## Overview
Rails 8 includes a built-in authentication generator that creates a complete, secure authentication system without external gems.
## Quick Start
```bash
# 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
```ruby
# 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
```ruby
# 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
```ruby
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end
```
### Authentication Concern
```ruby
# 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
```ruby
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
```ruby
# In controllers
Current.user
Current.user.email_address
# In views
<%= Current.user.email_address %>
# In models (use sparingly)
Current.user
```
### Login Flow
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
# 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](reference/sessions.md) for session management details
- See [current.md](reference/current.md) for Current attributes patterns
## Common Customizations
### Remember Me
```ruby
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
```ruby
# 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
```ruby
# app/controllers/sessions_controller.rb
rate_limit to: 10, within: 3.minutes, only: :create,
with: -> { redirect_to new_session_path, alert: "Too many attempts" }
```
This skill implements Rails 8 built-in authentication generator and supplies a complete, production-ready auth flow without external gems. It scaffolds User, Session, and Current models, controller concerns, and views for login, logout, and password resets. Use it to quickly add secure session management, cookie-based authentication, and easy controller protection.
Run the generator to create models (User, Session, Current), controllers (Sessions, Passwords), and an authentication concern included into ApplicationController. The concern resumes sessions from signed cookies, enforces authentication by default, and provides helpers to start and terminate sessions. The User uses has_secure_password, Session generates secure tokens, and Current exposes request-local user access.
How does the session token get stored and resumed?
The generator stores a session token in a signed, HttpOnly cookie. On each request the Authentication concern checks cookies.signed[:session_token], looks up the Session by token, and sets Current.session if found.
Can I allow public access to specific controller actions?
Yes. The Authentication concern provides allow_unauthenticated_access which calls skip_before_action for listed actions so those routes remain public.