An Epic Adventure: Mastering Sessions in Ruby on Rails
As a web developer, understanding how to handle user sessions is absolutely essential. Sessions allow you to keep track of user data as they navigate through your app, enabling critical functionality like user authentication, authorization, shopping carts, and more.
Fortunately, Ruby on Rails makes it relatively easy to work with sessions once you understand a few key concepts. In this post, we‘ll dive deep into everything you need to know to become a true session handling wizard!
What Exactly Is a Session? A Peek Behind the Curtain
But first, what exactly is a session? In the context of web development, a session is a way to persist data across multiple requests from the same client. Since HTTP is a stateless protocol, this extra layer is needed to tie multiple requests together.
Under the hood, here‘s roughly how it works:
- When a user first visits your app, Rails creates a new session and assigns it a unique session ID.
- This session ID is sent back to the user‘s browser as a cookie.
- On subsequent requests, the browser sends the session ID cookie back to your app.
- Rails uses the session ID to retrieve the associated session data.
This session data is where you can store information you want to persist across requests for a particular user – things like their user ID (if they‘re logged in), items in a shopping cart, and so on.
By default, Rails stores session data in cookies on the client-side. This has the advantage of being very lightweight and not requiring any setup on the server-side. However, it does have some limitations:
- Cookies have a size limit (typically 4KB), so you can‘t store too much data in them.
- Cookies are sent with every request, so if you store too much data in the session, it can impact performance.
- Cookie data is visible to the user (although Rails does sign the cookies to prevent tampering).
For these reasons, cookies are fine for most simple use cases, but if you need to store a lot of session data or have high security requirements, you might want to consider using a server-side session store instead. We‘ll cover some of those options later in this guide.
Enabling Sessions in Rails
By default, Rails already has sessions enabled out of the box. This magic happens through some middleware that‘s automatically included in your app‘s Gemfile:
# Gemfile
gem ‘actionpack‘
The actionpack gem includes the ActionDispatch::Session::CookieStore
middleware which enables cookie-based sessions by default in Rails apps.
If you want to use a different session store, you can configure it in config/initializers/session_store.rb
. For example, to use the database as your session store:
# config/initializers/session_store.rb
MyApp::Application.config.session_store :active_record_store
We‘ll discuss the pros and cons of different session stores in more detail later.
Setting Up Authentication with Sessions
One of the most common uses of sessions is to handle user authentication – keeping a user logged in as they navigate around your app. Let‘s walk through how you might set this up in a Rails app, step-by-step.
Step 1: Generate a User Model
First, you‘ll need a User
model to represent your app‘s users. You can generate this model with a command like:
rails generate model User name:string email:string password_digest:string
This will generate a migration to create a users
table with name
, email
, and password_digest
columns. The password_digest
column will store a hashed version of the user‘s password for security purposes.
Run the migration to create the users
table:
rails db:migrate
Step 2: Set Up Secure Password Authentication
To securely handle user passwords, you‘ll want to use the has_secure_password
method provided by the bcrypt
gem. This method will automatically hash passwords before storing them and provide an authenticate
method for checking passwords.
First, make sure you have the bcrypt
gem in your Gemfile:
# Gemfile
gem ‘bcrypt‘
Then add has_secure_password
to your User
model:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
end
Step 3: Create a Sessions Controller
Next, you‘ll need a controller to handle logging users in and out. Let‘s call it SessionsController
:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
log_in(user)
redirect_to user_path(user), notice: ‘Logged in!‘
else
flash.now.alert = ‘Invalid email or password‘
render :new
end
end
def destroy
log_out
redirect_to root_url, notice: ‘Logged out!‘
end
private
def log_in(user)
session[:user_id] = user.id
end
def log_out
session.delete(:user_id)
@current_user = nil
end
end
Here‘s what‘s going on in this controller:
- The
new
action renders a login form. - The
create
action handles the login form submission. It finds the user by email, authenticates them using theauthenticate
method provided byhas_secure_password
, and if successful, logs them in by storing their ID in the session. - The
destroy
action logs the user out by removing their ID from the session. - The
log_in
andlog_out
methods are private helper methods to handle the actual session manipulation.
Step 4: Set Up Login/Logout Routes
You‘ll need to set up routes for your login and logout actions. In your config/routes.rb
file:
# config/routes.rb
get ‘login‘, to: ‘sessions#new‘
post ‘login‘, to: ‘sessions#create‘
delete ‘logout‘, to: ‘sessions#destroy‘
This sets up routes for rendering the login form (sessions#new
), handling the login form submission (sessions#create
), and logging out (sessions#destroy
).
Step 5: Create Login Form
Now you need a form for users to actually log in. In app/views/sessions/new.html.erb
:
<!-- app/views/sessions/new.html.erb -->
<%= form_with url: login_path, local: true do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit ‘Log In‘ %>
<% end %>
This creates a simple form with email and password fields that submits to the login_path
(which maps to the sessions#create
action).
Step 6: Use Session Data to Conditionally Show Logged In State
Finally, you probably want to show different content to logged in vs logged out users. You can do this by checking the session[:user_id]
value.
A common pattern is to define a current_user
helper method in your ApplicationController
:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
private
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
helper_method :current_user
end
This method checks if there‘s a user_id
stored in the session. If there is, it finds the corresponding User
record and memoizes it in an instance variable. The helper_method
line makes this method available in your views.
Then, in your layout, you can conditionally show different content based on whether current_user
exists:
<!-- app/views/layouts/application.html.erb -->
<% if current_user %>
Welcome, <%= current_user.name %>!
<%= link_to ‘Log Out‘, logout_path, method: :delete %>
<% else %>
<%= link_to ‘Log In‘, login_path %>
<%= link_to ‘Sign Up‘, signup_path %>
<% end %>
And there you have it! A basic but complete authentication system using Rails sessions. Of course, there‘s a lot more you could do (like adding password reset functionality, "remember me" cookies, etc.), but this gives you a solid foundation.
Choosing a Session Store
As mentioned earlier, Rails provides several options for where to store your session data. Let‘s take a closer look at each option and when you might want to use it.
Cookie Store (default)
- Stores session data directly in cookies on the client-side
- Advantages:
- Lightweight and requires no server-side setup
- Disadvantages:
- Limited to 4KB of data
- Data is visible to the user (although signed to prevent tampering)
- Data is sent with every request, which can impact performance if too much is stored
- Good for:
- Simple applications with small amounts of session data
- Applications where security isn‘t a top priority
Active Record Store
- Stores session data in your database using Active Record
- Advantages:
- Allows for more data to be stored than cookies
- Data is not visible to the user
- Disadvantages:
- Requires a database table to store sessions
- Can impact performance if many database queries are needed to load session data
- Requires periodic cleanup of old sessions
- Good for:
- Applications that need to store more session data than cookies allow
- Applications with high security requirements
Redis Store
- Stores session data in Redis, an in-memory data store
- Advantages:
- Very fast performance
- Allows for more data to be stored than cookies
- Data is not visible to the user
- Disadvantages:
- Requires running a separate Redis server
- Data can be lost if the Redis server crashes or is restarted
- Good for:
- Applications with very high performance requirements
- Applications that need to store a lot of session data
Memcached Store
- Similar to Redis store but uses Memcached instead of Redis
- Advantages and disadvantages are similar to Redis store
- Good for similar use cases as Redis store
In my experience, the cookie store is sufficient for most applications. It‘s simple, requires no setup, and performs well. I would only reach for one of the server-side stores if I specifically needed to store a large amount of session data or had unusually high security requirements.
Session Security Best Practices
While sessions are an essential tool, they can also be a security risk if not used properly. Here are some best practices I always follow to keep my apps‘ sessions secure:
-
Always use HTTPS in production. This encrypts all data sent between the client and server, including session cookies. Without HTTPS, session cookies can be intercepted and stolen in a man-in-the-middle attack.
-
Set the
secure
flag on session cookies. This tells the browser to only send the cookie over HTTPS connections. In Rails, you can do this by settingconfig.force_ssl = true
in your production config. -
Set the
HttpOnly
flag on session cookies. This prevents the cookie from being accessed by client-side JavaScript, which can help prevent cross-site scripting (XSS) attacks. Rails sets this flag by default. -
Generate a new session ID on login. This helps prevent session fixation attacks, where an attacker sets a known session ID and tricks a user into logging in with it. In Rails, you can use the
reset_session
method to generate a new session ID. -
Expire old sessions. Over time, your session store can fill up with old, abandoned sessions. To prevent this, set an expiration time for sessions and periodically clean out expired ones. In Rails, you can set
config.session_store :cookie_store, expire_after: 14.days
to automatically expire sessions after 14 days. -
Don‘t store sensitive data in sessions. Session data is not encrypted by default, so it‘s not a good place to store things like credit card numbers or passwords. If you need to store sensitive data, encrypt it first or, better yet, don‘t store it in the session at all.
-
Consider using a session management framework. For more complex authentication needs, a battle-tested library or framework like Devise or Clearance can handle a lot of the security heavy lifting for you.
To put some numbers to these risks, consider these statistics:
- In a study of over 133,000 websites, only 11.4% used HTTPS by default (source)
- In the same study, only 5.4% of sites had the
secure
flag set on their cookies - Cross-site scripting (XSS) vulnerabilities, which
HttpOnly
cookies can help prevent, were present in 14% of websites in Q2 2018 (source)
So while these measures might seem like overkill, they‘re really the minimum due diligence for keeping your users‘ data safe.
Sessions and RESTful Design
As a full-stack developer, it‘s also important to understand how sessions fit into the larger picture of your application‘s architecture.
One architectural style that‘s become very popular in recent years, especially in the world of APIs, is REST (Representational State Transfer). REST is a set of principles for building scalable, maintainable web services.
One of the key principles of REST is statelessness. In a truly RESTful system, each request should contain all the information necessary to understand and complete that request. The server should not need to store any state about the client between requests.
At first glance, sessions might seem to violate this principle. After all, the whole point of a session is to persist state across multiple requests. But it‘s important to distinguish between application state and resource state.
Resource state is the data that defines the resource itself – for a blog post, this might include the title, body, author, etc. This is the kind of state that should be transferred in the body of a RESTful request.
Application state, on the other hand, is data that controls the interaction between the client and server – things like whether the user is logged in, what page of results they‘re on, etc. It‘s okay (and often necessary) to store this kind of state in a session.
So in a RESTful Rails app, it‘s fine (and common) to use sessions for things like authentication and pagination, while still aiming to keep your core resources stateless. This balance allows you to build maintainable, scalable systems while still providing a good user experience.
Conclusion
Sessions are a crucial tool in any web developer‘s toolkit. They allow us to build stateful features on top of the stateless HTTP protocol, enabling things like user authentication, shopping carts, and complex workflows.
In this guide, we‘ve taken a deep dive into how sessions work in Ruby on Rails. We‘ve covered:
- What sessions are and how they work under the hood
- How to enable and configure sessions in a Rails app
- Step-by-step instructions for building a user authentication system with sessions
- The different session storage options available in Rails and when to use each one
- Security best practices for working with sessions
- How sessions fit into RESTful application design
I hope this guide has given you a comprehensive understanding of sessions in Rails and the confidence to use them effectively in your own projects. Remember, with great power comes great responsibility – always prioritize the security and privacy of your users‘ data.
Happy coding, and may your sessions be plentiful and secure!