← Back to posts

Migrate from devise to Rails authentication

How to switch from devise to your own authentication system.

Rails generator

Run the following generator to create a migration, which we need to store the user password.

bin/rails g migration add_password_digest_to_users password_digest:string

to add the relevant column to you User model (if you're authenticating by a User model).

Here your migration, for now leave it without the NOT NULL constraint.

class AddPasswordDigestToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column :users, :password_digest, :string#, null: false
  end
end

Update your rails app to < 8.0 and run the generator

Only from Rails 8.0 onwards do we get the following generator, which you can now run:

bin/rails generate authentication

This will create all the necessary files (controllers, views, tests, etc.) for you to get going with your own authentication layer. Delete the migration file that creates a new user, since we already have that in place.

Remove devise callbacks

Remove from application_controller.rb: before_action :authenticate_user!

Also, ensure you remove all devise callbacks, such as before_action :authenticate_user! etc. in all the other controllers where you may have used them.

Authorization

If you are using Pundit for authorization, you must add the following method to your application controller:

    def pundit_user
      Current.user
    end

If you had this one:

    def skip_pundit?
      devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
    end

Switch this to this one (the authentication/ is because i've put my authentication controllers inside of an authentication/ module):

    def skip_pundit?
      params[:controller] == "authentication/sessions" || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
    end

Ensure backwards compatibility

The generator command should have created a new sessions_controller.rb. In there replace the current create action:

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

with this one:

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

    if user && user.password_digest.blank?
      redirect_to new_password_path(email: params[:email]), alert: "Please request a new password."
    elsif user = User.authenticate_by(params.permit(:email, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

It's maybe not the best of user flows, but this ensures that when your users come from the old devise system, they have to first create a new password, which is stored in the new password_digest column. Btw, note that i've changed the email_address to email, to make it compatible with devises old way. You'll have to ensure that you're always targeting email instead of email_address in all the newly generated forms.

With this flow, you'll also have to ensure that your email system works, so that user can recreate the password.

And finally, remove the gem devise from your gemfile and delete all the related initializers and views.