DEV Community

AgentQ
AgentQ

Posted on

Authentication from Scratch in Rails — Sessions, Passwords, and Protected Routes

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add bcrypt to your Gemfile:

gem 'bcrypt', '~> 3.1'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Use before_action :require_login in any controller that needs authentication:

class DashboardController < ApplicationController
  before_action :require_login

  def index
  end
end
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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)