The "Devise" Dilemma
We’ve all been there. You run rails new, and the very next command is setting up Devise. It’s the standard. It’s battle-tested.
But sometimes, it’s overkill.
Devise creates over 10 tables/routes/views you might not need. And the moment you want to customize a controller or change how the session behaves, you find yourself digging through documentation to override the "magic."
Rails actually ships with a built-in, secure authentication mechanism that takes about 15 minutes to set up. It’s called has_secure_password.
Here is how to build a production-ready login system with zero external auth dependencies.
Step 1: The Setup (Bcrypt)
Rails uses the bcrypt hashing algorithm to secure passwords. It’s likely already in your Gemfile, just commented out.
# Gemfile
gem 'bcrypt', '~> 3.1.7'
Run bundle install.
Step 2: The Model
We don't store passwords. We store a digest (a hashed version) of the password. Rails looks for a column specifically named password_digest.
Generate your User model:
rails g model User email:string password_digest:string
rails db:migrate
Now, enable the magic in your model:
# app/models/user.rb
class User < ApplicationRecord
# Adds methods to set and authenticate against a BCrypt password.
# This mechanism requires you to have a password_digest attribute.
has_secure_password
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 6 }, if: -> { new_record? || !password.nil? }
end
What did has_secure_password just do?
- It adds virtual attributes for
passwordandpassword_confirmation. - It validates that those two match.
- It hashes the password and stores it in
password_digestupon save. - It gives you an
authenticatemethod.
Step 3: Registration (Sign Up)
Let's create a controller to handle signing up.
# config/routes.rb
get '/signup', to: 'registrations#new'
post '/signup', to: 'registrations#create'
# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
# Log them in automatically after sign up
session[:user_id] = @user.id
redirect_to root_path, notice: "Welcome to the app!"
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
Step 4: Sessions (Log In / Log Out)
This is where beginners often get confused. "Logging in" isn't a database change; it's a browser state change. We store the user_id in the browser's cookie (the session).
# config/routes.rb
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
# The magic 'authenticate' method comes from has_secure_password
if user && 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
Step 5: The "Current User" Helper
Finally, we need to access the logged-in user from anywhere in the app. We do this in the ApplicationController.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user, :logged_in?
def current_user
# Memoization: only fetch from DB if @current_user is nil
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def logged_in?
!!current_user
end
def require_user
unless logged_in?
redirect_to login_path, alert: "You must be logged in to do that."
end
end
end
Why do this?
- Understanding: You now know exactly how cookies and password hashing interact.
- Performance: No extra middleware or modules loading in the background.
- Flexibility: Want to allow login via Username OR Email? Just change one line in
SessionsController. Want to add 2FA later? It's easier to build on top of this simple logic than to fight Devise's warden hooks.
Devise is fantastic for enterprise apps with complex OmniAuth needs. But for your next side project? Try rolling your own.
Have you ditched Devise recently? Tell me about your stack in the comments!
Top comments (0)