Introduction
HTTP is a stateless protocol โ each request is independent and the server has no memory of previous requests. Sessions solve this by maintaining state across requests, enabling features like user authentication, shopping carts, and multi-step forms.
Rails provides a robust session system built on top of cookies, with multiple storage backends and strong security defaults.
How Sessions Work
The basic flow:
- User submits login credentials
- Server validates credentials
- Server creates a session and stores user data (user ID, role, etc.)
- Server sends a session identifier to the browser as a cookie
- Browser includes the cookie in every subsequent request
- Server reads the cookie, looks up the session, and knows who the user is
Browser Rails Server
| |
| POST /login {email, password} |
|---------------------------------->|
| | validate credentials
| | create session
| 200 OK |
| Set-Cookie: _session_id=abc123 |
|<----------------------------------|
| |
| GET /dashboard |
| Cookie: _session_id=abc123 |
|---------------------------------->|
| | look up session
| 200 OK + dashboard content | user_id = 42
|<----------------------------------|
Rails Session Basics
Rails provides a session hash that persists across requests:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
session[:user_role] = user.role
redirect_to dashboard_path, notice: "Logged in successfully"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
end
end
def destroy
session.delete(:user_id)
session.delete(:user_role)
# Or clear everything:
reset_session
redirect_to root_path, notice: "Logged out"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user, :logged_in?
private
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
def logged_in?
current_user.present?
end
def require_login
unless logged_in?
redirect_to login_path, alert: "Please log in to continue"
end
end
end
Session Storage Options
Rails supports several session storage backends, each with different trade-offs:
1. Cookie Store (Default)
The entire session data is stored in an encrypted, signed cookie on the client:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_myapp_session',
expire_after: 2.weeks,
secure: Rails.env.production?, # HTTPS only in production
httponly: true, # not accessible via JavaScript
same_site: :lax
Pros:
- No server-side storage needed
- Scales horizontally without shared state
- Fast โ no database lookup per request
Cons:
- Limited to ~4KB
- Session data is visible to the client (though encrypted)
- Cannot invalidate individual sessions server-side
2. Cache Store
Sessions stored in Rails cache (Redis, Memcached):
Rails.application.config.session_store :cache_store,
key: '_myapp_session',
expire_after: 2.hours
Pros:
- Fast reads/writes
- Can store larger sessions
- Can invalidate sessions server-side
Cons:
- Sessions lost if cache is cleared
- Requires cache infrastructure
3. Active Record Store
Sessions stored in the database:
rails generate active_record:session_migration
rails db:migrate
Rails.application.config.session_store :active_record_store,
key: '_myapp_session'
Pros:
- Persistent โ survives server restarts
- Can query and manage sessions
- Can invalidate individual sessions
Cons:
- Slower โ database query on every request
- Requires database cleanup for expired sessions
4. Redis Store (Recommended for Production)
gem 'redis-session-store'
Rails.application.config.session_store :redis_session_store,
key: '_myapp_session',
redis: {
expire_after: 2.hours,
key_prefix: 'myapp:session:',
url: ENV['REDIS_URL']
}
Pros:
- Fast, scalable, persistent
- Can invalidate sessions
- Works well in multi-server deployments
Security Best Practices
Always Use reset_session After Login
Regenerating the session ID after login prevents session fixation attacks:
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session # generate new session ID
session[:user_id] = user.id
redirect_to dashboard_path
end
end
Store Minimal Data in Sessions
Only store what you need โ typically just the user ID:
# Good: store only the ID
session[:user_id] = user.id
# Bad: storing sensitive or large data
session[:user] = user.to_json # don't do this
session[:credit_card] = card_number # never do this
Set Appropriate Expiry
# Short-lived sessions for sensitive apps
Rails.application.config.session_store :cookie_store,
expire_after: 30.minutes
# Longer sessions with "remember me" functionality
def create
if params[:remember_me]
cookies.permanent[:user_token] = user.generate_remember_token
else
session[:user_id] = user.id
end
end
Protect Against CSRF
Rails includes CSRF protection by default. Make sure it’s enabled:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception # default in Rails
end
Secure and HttpOnly Cookies
Rails.application.config.session_store :cookie_store,
key: '_myapp_session',
secure: true, # only sent over HTTPS
httponly: true # not accessible via document.cookie
Implementing “Remember Me”
# User model
class User < ApplicationRecord
def generate_remember_token
token = SecureRandom.urlsafe_base64
update_column(:remember_token, BCrypt::Password.create(token))
token
end
def authenticated_by_token?(token)
BCrypt::Password.new(remember_token).is_password?(token)
end
def forget
update_column(:remember_token, nil)
end
end
# SessionsController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session
session[:user_id] = user.id
if params[:remember_me] == '1'
token = user.generate_remember_token
cookies.permanent.signed[:remember_token] = {
value: "#{user.id}:#{token}",
httponly: true,
secure: Rails.env.production?
}
end
redirect_to dashboard_path
end
end
# ApplicationController
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:remember_token]
user_id, token = cookies.signed[:remember_token].split(':')
user = User.find_by(id: user_id)
if user&.authenticated_by_token?(token)
session[:user_id] = user.id
@current_user = user
end
end
end
Debugging Sessions
# In a controller or view (development only)
Rails.logger.debug "Session: #{session.to_hash}"
# In rails console
# Check session data for a specific session ID
ActiveRecord::SessionStore::Session.find_by(session_id: 'abc123')
Summary
| Storage | Speed | Persistence | Scalability | Invalidation |
|---|---|---|---|---|
| Cookie store | Fast | Client-side | Excellent | No |
| Cache store | Fast | Volatile | Good | Yes |
| Database store | Slow | Persistent | Poor | Yes |
| Redis store | Fast | Persistent | Excellent | Yes |
For most production Rails apps, Redis is the recommended session store โ it’s fast, persistent, and supports session invalidation.
Resources
- Rails Security Guide: Sessions
- Rails Session Storage
- How Rails Sessions Work โ Justin Weiss
- OWASP Session Management Cheat Sheet
Comments