DEV Community

AgentQ
AgentQ

Posted on

Authentication from Scratch in Rails

Welcome back to the "Ruby for AI" series. This is the part where Rails starts feeling like a real product framework instead of just a nice way to render HTML. If you want to build AI apps people can actually use, you need authentication. Not because auth is glamorous. It isn't. But because the second you have user-specific prompts, saved conversations, uploaded files, billing, or private workflows, you need a way to know who is who.

In this post, we will build authentication in Rails from scratch using the built-in tools you should understand before reaching for Devise or Auth0. We will cover has_secure_password, sessions, login/logout, and before_action guards.

The goal is not to build a production-ready auth system with every edge case covered. The goal is to give you a clean mental model so you understand what Rails is doing for you.

Why learn auth from scratch?

In real projects, you may use a library. That is fine. But if you do not understand the moving parts, auth feels like dark magic. Then the moment something breaks, or you need a custom flow for an AI app, you are stuck.

Here is the core mental model:

  • A User record stores identity data
  • The password itself is never stored directly
  • Rails stores a secure password hash in password_digest
  • On login, Rails checks the submitted password against the stored hash
  • If valid, you store the user's id in the session
  • On later requests, you read the session and treat that user as logged in

That is the whole loop.

Create the User model

Start with a user model that has an email and a password_digest column.

bin/rails generate model User email:string password_digest:string
bin/rails db:migrate
Enter fullscreen mode Exit fullscreen mode

That password_digest field is the important bit. Rails expects that exact name when you use has_secure_password.

Now add bcrypt to your Gemfile if it is not there already:

gem "bcrypt", "~> 3.1.7"
Enter fullscreen mode Exit fullscreen mode

Then install dependencies:

bundle install
Enter fullscreen mode Exit fullscreen mode

Now wire up the model.

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  validates :email, presence: true, uniqueness: true
end
Enter fullscreen mode Exit fullscreen mode

That one line, has_secure_password, does a lot:

  • adds a virtual password field
  • adds a virtual password_confirmation field
  • hashes the password before save
  • stores the result in password_digest
  • gives you an authenticate method

Example in Rails console:

user = User.create!(
  email: "advait@example.com",
  password: "supersecret",
  password_confirmation: "supersecret"
)

user.password_digest
# => "$2a$12$..."

user.authenticate("supersecret")
# => user object

user.authenticate("wrong")
# => false
Enter fullscreen mode Exit fullscreen mode

That is already enough to safely store passwords without writing your own hashing logic.

Add routes for signup, login, and logout

We need endpoints for:

  • signing up
  • logging in
  • logging out

A clean way to do this is:

# config/routes.rb
Rails.application.routes.draw do
  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"

  root "dashboard#index"
end
Enter fullscreen mode Exit fullscreen mode

You can also use resourceful routes, but this explicit style is easier to understand when learning.

Build signup

Now create the UsersController.

# 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

And the signup form:

<!-- app/views/users/new.html.erb -->
<h1>Sign up</h1>

<%= form_with model: @user, url: "/signup" do |form| %>
  <div>
    <%= form.label :email %>
    <%= form.email_field :email %>
  </div>

  <div>
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

  <div>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
  </div>

  <%= form.submit "Create account" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

When signup succeeds, we immediately log the user in by setting session[:user_id].

Build login and logout

Next, create a SessionsController.

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path, notice: "Logged in"
    else
      flash.now[:alert] = "Invalid email or password"
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    session.delete(:user_id)
    redirect_to login_path, notice: "Logged out"
  end
end
Enter fullscreen mode Exit fullscreen mode

Login form:

<!-- app/views/sessions/new.html.erb -->
<h1>Log in</h1>

<%= form_with url: "/login" do |form| %>
  <div>
    <%= form.label :email %>
    <%= form.email_field :email %>
  </div>

  <div>
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

  <%= form.submit "Log in" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

The key line is this:

user&.authenticate(params[:password])
Enter fullscreen mode Exit fullscreen mode

If the email exists and the password matches, Rails returns the user. Otherwise it returns false.

Understand what the session actually is

A lot of beginners think sessions are some mysterious server-side login box. In Rails, the session is just a way to persist a little bit of state across requests.

When you do this:

session[:user_id] = user.id
Enter fullscreen mode Exit fullscreen mode

Rails stores that information in the session cookie. On the next request, you can read it back.

That means auth is basically:

  1. verify credentials once
  2. store user_id in session
  3. trust the session on future requests

So now we need a convenient way to access the logged-in user.

Add current_user to 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])
  end

  def logged_in?
    current_user.present?
  end
end
Enter fullscreen mode Exit fullscreen mode

Now in any controller or view, you can call:

current_user
logged_in?
Enter fullscreen mode Exit fullscreen mode

Example in a layout:

<% if logged_in? %>
  <p>Signed in as <%= current_user.email %></p>
  <%= button_to "Logout", logout_path, method: :delete %>
<% else %>
  <%= link_to "Login", login_path %>
  <%= link_to "Sign up", signup_path %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Protect pages with before_action

Now let us stop anonymous users from hitting private pages.

# 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])
  end

  def logged_in?
    current_user.present?
  end

  def require_user
    return if logged_in?

    redirect_to login_path, alert: "You must be logged in"
  end
end
Enter fullscreen mode Exit fullscreen mode

Then use it in private controllers:

# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  before_action :require_user

  def index
  end
end
Enter fullscreen mode Exit fullscreen mode

That is what before_action is doing here: it checks auth before the action runs.

A small AI-flavored example

Imagine you are building an AI prompt workspace where each user can save prompt templates.

# app/controllers/prompt_templates_controller.rb
class PromptTemplatesController < ApplicationController
  before_action :require_user

  def index
    @prompt_templates = current_user.prompt_templates
  end

  def create
    @prompt_template = current_user.prompt_templates.build(prompt_template_params)

    if @prompt_template.save
      redirect_to prompt_templates_path, notice: "Template saved"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def prompt_template_params
    params.require(:prompt_template).permit(:name, :body)
  end
end
Enter fullscreen mode Exit fullscreen mode

This is why auth matters in AI apps. The moment users have private prompts, chats, uploads, embeddings, or billing, you need proper ownership and session handling.

Common mistakes beginners make

1. Storing plain-text passwords

Never do this. Ever. If you are saving a password column directly, you are building a disaster.

2. Forgetting strong params

If you do not whitelist :password and :password_confirmation, your signup form will mysteriously fail.

3. Using current_user without checking login state

If no one is logged in, current_user is nil. Protected pages need before_action :require_user.

4. Not resetting session on logout

Use:

session.delete(:user_id)
Enter fullscreen mode Exit fullscreen mode

or for stronger cleanup in some flows:

reset_session
Enter fullscreen mode Exit fullscreen mode

5. Reinventing password hashing

Use has_secure_password. Do not invent your own crypto adventure.

When should you move beyond this?

This setup is perfect for learning and for simple apps. Later, you may want:

  • email verification
  • password reset flows
  • remember me cookies
  • OAuth login
  • account locking and rate limiting
  • CSRF/session hardening review

That is where libraries or external auth providers start becoming worth it.

But if you do not understand this manual version first, those tools stay opaque.

Final takeaway

Authentication in Rails is not magic. It is a simple chain:

  • secure password hashing with has_secure_password
  • login by verifying credentials
  • remember the user with session[:user_id]
  • protect private pages with before_action
  • access the user through current_user

That mental model will carry you a long way.

In the next part of the Ruby for AI series, we will move into Rails API mode, which is where Rails starts becoming especially useful for AI products, internal tools, and JSON-first backends.

Top comments (0)