Authentication in Rails doesn't need to be magic. You're going to build it from scratch so you understand every piece.
By the end, you'll have sessions, passwords, and protected routes working. No gems that hide the logic.
Why Build It Yourself?
Devise is great for production. But if you don't understand what it's doing, you're cargo-culting security. Build it once manually, then decide if you want the abstraction.
The Setup
Start with a user model:
rails generate model User email:string password_digest:string
rails db:migrate
Add has_secure_password to your model:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: { case_sensitive: false }
validates :password, length: { minimum: 8 }, if: -> { new_record? || !password.nil? }
end
Add bcrypt to your Gemfile:
gem 'bcrypt', '~> 3.1'
Run bundle install. has_secure_password gives you password and password_confirmation setters, automatic hashing, and an authenticate method.
The Controllers
Sessions controller handles login/logout:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email].downcase)
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in successfully"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "Logged out"
end
end
Users controller for registration:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
session[:user_id] = @user.id
redirect_to root_path, notice: "Account created"
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
Protected Routes
Add this to your ApplicationController:
# 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
end
def require_login
unless logged_in?
redirect_to login_path, alert: "Please log in first"
end
end
end
Use before_action :require_login in any controller that needs authentication:
class DashboardController < ApplicationController
before_action :require_login
def index
end
end
The Views
Login form (app/views/sessions/new.html.erb):
<h1>Log In</h1>
<%= form_with url: login_path do |f| %>
<div>
<%= f.label :email %>
<%= f.email_field :email, required: true %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password, required: true %>
</div>
<%= f.submit "Log In" %>
<% end %>
Registration form (app/views/users/new.html.erb):
<h1>Sign Up</h1>
<%= form_with model: @user do |f| %>
<% if @user.errors.any? %>
<div class="errors">
<% @user.errors.full_messages.each do |msg| %>
<p><%= msg %></p>
<% end %>
</div>
<% end %>
<div>
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<%= f.submit "Create Account" %>
<% end %>
Routes
# config/routes.rb
Rails.application.routes.draw do
root "home#index"
get "/signup", to: "users#new"
post "/signup", to: "users#create"
get "/login", to: "sessions#new"
post "/login", to: "sessions#create"
delete "/logout", to: "sessions#destroy"
end
Security Checklist
- Passwords are hashed with bcrypt (cost factor 12+)
- Session IDs are random and stored server-side
- CSRF protection is enabled (Rails default)
- Password minimum length enforced
- Email uniqueness is case-insensitive
- Failed login doesn't reveal if email exists
What About "Remember Me?"
Add a remember_token to users, store hashed version in DB, set a signed cookie:
# In SessionsController#create for "remember me"
cookies.signed[:remember_token] = { value: user.remember_token, expires: 2.weeks }
Check for it in current_user if session is empty. Clear it on logout.
Next Up: Rails API Mode
Now you understand sessions. But what if you're building a JSON API for a SPA or mobile app? That's where token authentication comes in. We'll cover that next.
Series: Ruby for AI — Part 10 of 34
Top comments (0)