DEV Community

Cover image for Devise to Custom Auth: It’s Not Just `has_secure_password
Zil Norvilis
Zil Norvilis

Posted on

Devise to Custom Auth: It’s Not Just `has_secure_password

The Itch to Switch

Devise is the 800-pound gorilla of Rails authentication. It’s reliable, secure, and does everything.

But eventually, you hit "The Wall."
Maybe you want a passwordless login flow. Maybe you're tired of overriding 15 different controller methods just to change a redirect. Maybe you just want to understand how your own app logs people in.

So you decide: "I’m going to rip out Devise and use Rails' built-in has_secure_password."

I’ve done this migration. It leads to a cleaner, faster codebase—but the path there is filled with landmines. Here are the biggest challenges you will face.

Challenge 1: The Database Schema Mismatch

Devise is opinionated about database columns.

  • Devise uses: encrypted_password
  • Rails (has_secure_password) expects: password_digest

You have two options here, and both have trade-offs.

Option A: The Alias (Quick & Dirty)
You can tell your User model that password_digest is actually encrypted_password.

class User < ApplicationRecord
  has_secure_password :password, validations: false
  alias_attribute :password_digest, :encrypted_password
end
Enter fullscreen mode Exit fullscreen mode
  • The risk: It feels hacky. Any future gems or tools expecting standard Rails conventions might stumble.

Option B: The Migration (Clean & Risky)
You write a migration to rename the column.

rename_column :users, :encrypted_password, :password_digest
Enter fullscreen mode Exit fullscreen mode
  • The risk: You better make sure you don't have any downtime during this deployment. Also, if you have to rollback, things get messy fast.

Challenge 2: The "Recoverable" Module

Devise gave you "Forgot Password" for free. Now that you deleted it, no one can reset their password.

Writing a secure password reset flow is harder than it looks. You need to:

  1. Generate a unique, high-entropy token.
  2. Store a digest of that token (never store the raw token in the DB!).
  3. Set an expiration (e.g., 2 hours).
  4. Handle the email delivery.
  5. Crucial: Ensure the token is invalidated after use so it can't be replayed.

Devise handled the "timing attack" prevention and token hashing for you. You now own that security liability.

Challenge 3: Cookie Security & "Remember Me"

session[:user_id] is fine for a basic login. But what about "Remember Me"?

Devise's Rememberable module is surprisingly complex. It manages a persistent cookie, token rotation (sometimes), and secure comparison.

If you roll your own "Remember Me":

  • You must sign/encrypt the cookie.
  • You must worry about Session Hijacking. If an attacker steals that cookie, they are that user forever until the cookie expires.
  • You need a way to "Invalidate all sessions" (e.g., if the user changes their password, all those old remember tokens need to die).

Challenge 4: The View & Controller Cleanup

This is the tedious part. Devise helpers are insidious—they get everywhere.

You will have to grep your entire project for:

  • authenticate_user! (Replace with your own require_login filter)
  • user_signed_in? (Replace with logged_in?)
  • current_user (You have to write this helper in ApplicationController)
  • devise_error_messages! (You'll need to write your own error partials)

Pro Tip: Do not delete the Gem yet. Create a "Shim" module that maps the old Devise method names to your new logic so you don't have to change 500 files at once.

# app/controllers/concerns/auth_shim.rb
module AuthShim
  def authenticate_user!
    redirect_to login_path unless logged_in?
  end

  def user_signed_in?
    logged_in?
  end
end
Enter fullscreen mode Exit fullscreen mode

Challenge 5: Legacy Password Compatibility

Devise uses BCrypt by default. Rails has_secure_password also uses BCrypt. Usually, they play nice.

However:
If your Rails app is old (migrated from Rails 3 or 4), Devise might be using an older hashing strategy (like SHA-1 with a salt) for older users. has_secure_password will not be able to authenticate those users.

The Fix:
You might need a custom authentication strategy that checks:

  1. Is this a BCrypt hash? Use authenticate.
  2. Is this a legacy hash? specific logic.
  3. If legacy, upgrade them to BCrypt upon successful login.

The Verdict: Is it worth it?

Don't do it if:

  • You are an agency building an MVP for a client (Speed is key).
  • You rely heavily on OmniAuth (Devise makes OAuth integration very smooth).
  • You aren't comfortable with security concepts like Session Fixation or Timing Attacks.

Do it if:

  • You are building a monolithic app intended to last 5+ years.
  • You want a specialized flow (e.g., OTP only, magic links, multi-step onboarding).
  • You want to reduce memory footprint and dependency bloat.

Removing Devise is one of the most educational things you can do in Rails. Just make sure you bring a flashlight—it’s dark down in the plumbing.


Have you survived a Devise removal? Share your war stories below! 👇

Top comments (0)