The go-to resource for upgrading Ruby, Rails, and your dependencies.

Replacing Devise with Rails 8.0 Built-In Authentication: A Step-by-Step Guide


Learn how to reduce technical debt and migrate your Ruby on Rails application from Devise to the built-in authentication system in Rails 8.0.

The Evolution of Rails Authentication

In the early days of Rails, authentication was often a manual process, aided by plugins like acts_as_authenticated or restful_authentication. Over time, though, the community consolidated around Devise. Devise provided a comprehensive, battle-tested solution for everything from basic password management to complex lockable and trackable modules. This comprehensive approach saved developers thousands of hours of initial development time.

Over time, however, maintaining heavy dependencies introduces its own costs. Customizing Devise controllers often requires reading through deep layers of library code, and keeping the gem updated through major Rails upgrades can require significant effort. The cost of maintaining a Rails application decreases when we rely on the framework’s native primitives rather than external abstractions.

With the release of Rails 8.0, the framework introduces a built-in authentication generator. Rather than adding a monolithic dependency, running bin/rails generate authentication provides a foundation built directly on has_secure_password and standard Rails controllers. This approach is designed to give you complete ownership of your authentication flow.

If you are planning a migration to the new version of Rails, replacing Devise with these native primitives is a high-value strategy for reducing technical debt. Before we get into that, though, we need to understand the mechanics of the migration.

Understanding the Architectural Shift

Devise operates as a black box engine. It injects routes, provides hidden controllers, and adds extensive modules to your user model.

The Rails 8 approach, on the other hand, relies on code generation. The framework generates the actual controllers (SessionsController, PasswordsController), mailers, and models directly into your app/ directory. You own the code. If you need to change how a password reset email looks or how a user is routed after login, you edit your own files rather than overriding a gem’s behavior.

We can migrate existing Devise users to this new system without requiring them to reset their passwords. Devise uses the bcrypt hashing algorithm under the hood. Rails has_secure_password also uses bcrypt. This means the underlying password hashes are completely compatible.

Step 1: The Pre-Flight Check

Of course, migrating an authentication system is a significant undertaking. Before making any changes, we must assess our current usage of Devise. If your application heavily relies on modules like Omniauthable for complex single sign-on (SSO) integrations, Lockable for brute-force protection, or Invitable, you will need to manually rebuild those features. In such cases, you must reconsider if migrating away from Devise is the right move for your specific use case.

For applications primarily using DatabaseAuthenticatable, Recoverable, and Registerable, the migration path is straightforward. Ensure you have a solid test suite covering your authentication flows before proceeding.

Step 2: Generating the New Authentication Scaffold

First, we generate the Rails 8 authentication scaffold. This command creates the necessary controllers, models, and mailers for session and password management.

$ bin/rails generate authentication

This command generates several files. You also may notice it creates a migration for a sessions table:

  • app/models/session.rb
  • app/controllers/sessions_controller.rb
  • app/controllers/passwords_controller.rb
  • app/models/current.rb
  • app/controllers/concerns/authentication.rb
  • db/migrate/YYYYMMDDHHMMSS_create_sessions.rb

Run this migration to update your database:

$ bin/rails db:migrate

Step 3: Migrating the Database Schema

Devise stores the user’s password hash in a column named encrypted_password. The Rails has_secure_password method, on the other hand, expects this column to be named password_digest. We have two options: tell Rails to use the old column name, or rename the column. Renaming the column is the cleaner, long-term approach.

Generate a migration to rename the column:

$ bin/rails generate migration RenameDeviseColumns

Then, update the generated migration file:

class RenameDeviseColumns < ActiveRecord::Migration[8.0]
  def change
    rename_column :users, :encrypted_password, :password_digest
    
    # We also remove columns specific to Devise that we no longer need.
    remove_column :users, :reset_password_token, if_exists: true
    remove_column :users, :reset_password_sent_at, if_exists: true
    remove_column :users, :remember_created_at, if_exists: true
  end
end

Warning: Before removing columns from a production database, it is wise to ensure you have a complete backup. You must assume that any data in these columns will be permanently lost once the migration runs.

Run the migration:

$ bin/rails db:migrate

Step 4: Updating the User Model

Next, we must update the User model. We will remove the Devise configuration and replace it with the Rails 8 primitives.

Open app/models/user.rb and modify it:

class User < ApplicationRecord
  # Remove the Devise configuration:
  # devise :database_authenticatable, :registerable,
  #        :recoverable, :rememberable, :validatable

  # Add the Rails 8 native authentication:
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

One may wonder: if we rename the column and change the authentication mechanism, will users still be able to log in? The answer is straightforward. Because both systems use bcrypt, your users will still be able to log in using their existing passwords. The hashes stored in the newly renamed password_digest column will be validated perfectly by has_secure_password.

Step 5: Swapping the Controllers and Helpers

Devise provides helper methods like authenticate_user! and current_user. The Rails 8 generator, however, uses a different convention based on the Authentication concern it generated.

You must search your codebase for Devise-specific methods and replace them:

  1. Replace before_action :authenticate_user! with allow_unauthenticated_access or require_authentication based on the new Rails 8 concern patterns.
  2. Replace current_user with Current.user (or the specific helper method defined in your new Authentication concern, often Current.session&.user).
  3. Replace user_signed_in? with a check for the presence of the user object, like Current.user.present?.

For example, your ApplicationController might need to include the new concern if it does not already:

class ApplicationController < ActionController::Base
  include Authentication
  
  # By default, we require authentication for all actions
  before_action :require_authentication
end

Then, in controllers that should be public, you use the new helper to opt out:

class PublicPagesController < ApplicationController
  allow_unauthenticated_access
  
  def home
  end
end

Step 6: Removing the Devise Dependency

Once you have updated your routes, controllers, and views to use the new native systems — and critically, once your test suite passes — you can safely remove Devise.

Remove the gem from your Gemfile:

# Remove or comment out this line:
# gem 'devise'

Then, bundle your application and remove the Devise initializer:

$ bundle install
$ rm config/initializers/devise.rb

You should also remove any custom Devise views located in app/views/devise/ if you previously generated them.

Trade-Offs and Long-Term Implications

The Rails 8 built-in authentication is a lightweight solution that reduces technical debt hotspots. It gives you complete ownership of your codebase and makes future framework upgrades smoother.

Strictly speaking, though, this native approach is not a feature-for-feature replacement for Devise. If your application relies on comprehensive auditing, complex locking strategies, or deep OmniAuth integration, you will need to build those features yourself or continue relying on specialized gems. For many standard web applications, however, moving to the native primitives provides a cleaner, more maintainable foundation.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming