Rails authentication: a Ruby on Rails guide for developers
Authentication is one of the first features you'll implement in any Rails application, yet it's easy to get wrong if you're not careful about session handling, password storage, and user verification. Rails 8 introduced a built-in authentication generator that gives you a solid foundation without the overhead of heavier gems, making it the right choice for many teams. In this guide, we'll walk through the native Rails authentication tools, explore when to reach for additional gems, and show you how to build secure systems that scale across your application.
Understanding Rails authentication fundamentals
Rails authentication revolves around three core concepts: identifying who a user is, verifying their credentials, and maintaining that identity across requests. The framework provides several layers of tools to handle these responsibilities, from low-level password hashing to session management. Before Rails 8, you'd either build authentication from scratch or use Devise, a heavy-duty gem that solved nearly every authentication problem imaginable. Today, the built-in generator offers a pragmatic middle ground that works for most applications.
The key to understanding Rails authentication is recognizing that it's composed of three separate concerns: password security, session management, and authorization checks. We'll explore each as we walk through the generator, has_secure_password, and session handling.
Generator: bin/rails generate authentication
Rails 8's authentication generator creates a complete authentication scaffold with minimal configuration. It generates a user model with password handling, a sessions controller for login and logout, and middleware that makes the current user available throughout your application. This is a significant improvement over previous versions because it gives you working code without requiring external dependencies beyond what Rails already provides.
Here's how to generate authentication:
bin/rails generate authenticationThis single command creates:
- A
Usermodel with email and password_digest columns - Migration files to set up the users table
- A
SessionsControllerfor login and logout flows - A
Currentclass for accessing the authenticated user - Routes for authentication endpoints
- Helper methods like
authenticate_user!andcurrent_user
Let's walk through what the generator produces. Your User model will include has_secure_password, which we'll cover in detail next. The migration creates:
create_table :users do |t|
t.string :email_address, null: false
t.string :password_digest, null: false
t.timestamps
end
add_index :users, :email_address, unique: trueThe unique constraint on email ensures each user can only have one account. After running migrations, you have a working authentication system ready to integrate into your controllers and views. The generator also sets up routes automatically, so POST /sessions handles login and DELETE /sessions handles logout.
One team we worked with on a SaaS platform used this generator as their starting point and added custom fields (like a subscription_tier) to the User model afterward. The generator doesn't lock you in; it's a foundation you extend. You can add password reset flows, two-factor authentication, or OAuth integration once the basics are working.
has_secure_password: bcrypt password hashing
has_secure_password is a Rails method that handles password hashing and validation. It lives in your User model and uses bcrypt, a proven hashing algorithm that's intentionally slow to defend against brute-force attacks. This is critical: never store passwords in plain text, and never use fast hashing algorithms like SHA-256 for passwords.
When you add has_secure_password to your model:
class User < ApplicationRecord
has_secure_password
validates :email_address, presence: true, uniqueness: true
validates :password, length: { minimum: 12 }
endRails automatically:
- Adds a virtual
passwordattribute (not persisted to the database) - Adds a
password_confirmationattribute for validation - Hashes the password using bcrypt before saving to
password_digest - Provides an
authenticate(password)method to verify passwords at login - Validates that
passwordandpassword_confirmationmatch
Here's how login works in practice. Your SessionsController receives a username and password, then calls:
class SessionsController < ApplicationController
def create
user = User.find_by(email_address: params[:email_address])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in successfully"
else
redirect_to new_session_path, alert: "Invalid credentials"
end
end
def destroy
session.delete(:user_id)
redirect_to root_path, notice: "Logged out"
end
endThe authenticate method compares the provided password against the stored bcrypt hash. If they match, it returns the user object; otherwise, it returns nil. Notice we use the safe navigation operator &. to avoid raising an error when the user doesn't exist, which protects against timing attacks that could reveal whether an email is registered.
Bcrypt's cost factor determines how slow the hashing is. Rails defaults to a cost of 12, which takes roughly 200ms on modern hardware. This makes it expensive to crack passwords through brute force. You can adjust this in your initializers, but the default is sensible for most applications.
Sessions: Cookie-based session management
Rails sessions are stored on the client as encrypted cookies by default. When you set session[:user_id] = user.id, Rails serializes that data, encrypts it with your application's secret key, and sends it to the browser. On subsequent requests, the browser sends the encrypted cookie back, Rails decrypts it, and your controllers can access the user ID.
This cookie-based approach is stateless from the server's perspective, which is why it scales well. There's no session table to query; everything you need is in the cookie. However, you can optionally store sessions in a database or cache, which is useful if you need to invalidate sessions server-side (for instance, after a logout).
To use database-backed sessions, configure your application:
config.sessions_store :active_record_storeThen generate and run the migration:
bin/rails generate active_record:session_store
bin/rails db:migrateDatabase sessions are heavier than cookies but give you control. When a user logs out, you delete their session record, and their old cookie becomes invalid. This is essential for logout workflows where you want to revoke access immediately.
The generated Current class (from the authentication generator) provides a clean interface for accessing the current user:
class Current < ActiveSupport::CurrentAttributes
attribute :user
def self.user=(user)
super
Time.zone = user.time_zone if user
end
endYou set the current user in a before_action callback in your ApplicationController:
class ApplicationController < ActionController::Base
before_action :set_current_user
helper_method :current_user, :user_signed_in?
private
def set_current_user
Current.user = User.find_by(id: session[:user_id]) if session[:user_id]
end
def user_signed_in?
Current.user.present?
end
def authenticate_user!
redirect_to new_session_path unless user_signed_in?
end
endNow any controller can call current_user or authenticate_user! to guard routes. This pattern works across a single Rails app, but if you're running multiple app instances or a microservices setup, consider JWT or OAuth tokens instead of cookies.
Devise: When to reach for a full-featured gem
Devise is a mature, battle-tested authentication gem that's been the Rails standard for over a decade. It provides password reset flows, email confirmation, account lockout after failed attempts, multi-factor authentication modules, and OAuth integration out of the box. If your authentication needs are complex, Devise saves weeks of development.
We recommend Devise if your application requires:
- Email confirmation workflows
- Password reset via email
- Account lockout after failed login attempts
- OAuth integrations (Google, GitHub, etc.)
- Two-factor authentication
- Multiple user roles or permissions systems
- Admin interfaces for user management
For example, a travel platform we built used Devise because users needed email confirmation before booking, password reset functionality, and OAuth login via Google and Facebook. The overhead was justified because these features were core to the product.
However, Devise can feel like overkill for simple applications. If you're building an internal tool, a small API, or a proof of concept, the Rails 8 generator is sufficient. You can always migrate to Devise later if requirements grow.
If you do use Devise, install it with:
bundle add devise
bin/rails generate devise:install
bin/rails generate devise user
bin/rails db:migrateDevise handles the rest. It even generates views for login, registration, and password reset, though you'll customize them for your brand.
Common mistakes
- Storing passwords in plain text or using fast hashing: Always use
has_secure_passwordwith bcrypt. Never roll your own password security. - Forgetting to validate email uniqueness: Multiple accounts with the same email create confusion and security issues. Always add a unique constraint and validation.
- Using unsafe navigation in login checks: The safe navigation operator
&.prevents timing attacks. Don't writeif user.authenticate(...)because it raises an error if the user doesn't exist, which tells attackers which emails are registered. - Not validating password length: Short passwords are easy to crack. Rails 8's generator defaults to 12 characters; respect that minimum.
- Storing sensitive data in cookies: Cookies are encrypted but not secret. Don't store role names, permission flags, or payment info in session data. Fetch that from the database on each request.
- Ignoring logout workflows: If you use database sessions, ensure logout deletes the session record. With cookie-only sessions, logout just clears the cookie, but the old cookie could theoretically be used until it expires.
- Not setting secure and httponly flags: Rails does this by default in production, but ensure your session configuration includes
secure: true(HTTPS only) andhttp_only: true(no JavaScript access).
Key takeaways
- Rails 8's authentication generator provides a pragmatic starting point for most applications without external gem dependencies.
has_secure_passworduses bcrypt to hash passwords securely; never implement password hashing yourself.- Rails sessions are encrypted cookies by default, making them stateless and scalable. Use database sessions only if you need server-side invalidation.
- Use the
Currentclass pattern to access the current user cleanly throughout your application. - Devise is worth adopting when you need email confirmation, password reset, OAuth, or multi-factor authentication. For simple auth, the generator is sufficient.
- Always validate email uniqueness, enforce minimum password length, and use safe navigation operators in authentication checks.
- Never store sensitive application data in cookies; always fetch it from the database.






Comments
Loading comments…