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
Userrecord 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
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"
Then install dependencies:
bundle install
Now wire up the model.
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
That one line, has_secure_password, does a lot:
- adds a virtual
passwordfield - adds a virtual
password_confirmationfield - hashes the password before save
- stores the result in
password_digest - gives you an
authenticatemethod
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
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
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
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 %>
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
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 %>
The key line is this:
user&.authenticate(params[:password])
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
Rails stores that information in the session cookie. On the next request, you can read it back.
That means auth is basically:
- verify credentials once
- store
user_idin session - 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
Now in any controller or view, you can call:
current_user
logged_in?
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 %>
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
Then use it in private controllers:
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
before_action :require_user
def index
end
end
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
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)
or for stronger cleanup in some flows:
reset_session
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)