Devise has been my go-to authentication library for as long as I can remember. Recently, though, I had requirements that were incompatible with Devise. Namely, to allow authentication both from a HTML frontend via session cookies, and via JWT from a mobile app.

Apparently, Rodauth supports that use case and many more.

This post explains the minimal steps to migrate from Devise to Rodauth in a Rails application, while the other posts of the series explain advanced topics when special Devise features were in use.

Devise removal

Run bundle remove devise to remove the Devise gem. To add Rodauth, run bundle add rodauth-rails.

- gem 'devise'

+ gem 'rodauth-rails'

Usually, the User model defines what Devise modules are loaded. Remove the entire line containing the modules in app/models/user.rb:

- devise :database_authenticatable, :trackable, :recoverable, :rememberable, :validatable

The routes file at config/routes.rb mounts the Devise engine. Remove the entire line:

- devise_for :users

Lastly, remove the devise initializer at config/initializers/devise.rb.

Rodauth generator

To install the required files for Rodauth, run the generator.

rails generate rodauth:install users

You can customize the model for which the configuration is generated by exchanging users with whatever model you need.

The generator creates and modifies multiple files:

  • adds rodauth_app.rb and rodauth_main.rb in app/misc
  • adds the controller in app/controllers/rodauth_controller.rb
  • modifies the user model in app/models/user.rb
  • adds the initializer in config/initializers/rodauth.rb
  • adds the migration in db/migrate/create_rodauth.rb

Database migration

The only change we need to apply to those generated files concerns the migration. Since we already have a users table, we can’t run the migration containing a create_table command for the users table.

To fix the migration, remove the create_table command and replace it with the respective commands that modify our table to match the schema:

- create_table :users do |t|
-   t.integer :status, null: false, default: 1
-   t.citext :email, null: false
-   t.check_constraint "email ~ '^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$'", name: "valid_email"
-   t.index :email, unique: true, where: "status IN (1, 2)"
-   t.string :password_hash
- end

+ add_column :users, :status, :integer, null: false, default: 1
+ change_column :users, :email, :citext
+ add_check_constraint :users, "email ~ '^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$'", name: "valid_email"
+ remove_index :users, :email, unique: true
+ add_index :users, :email, unique: true, where: "status IN (1, 2)"
+ rename_column :users, :encrypted_password, :password_hash

Run the migration and check if it completes successfully: rails db:migrate.

View and controllers

Rodauth has different method names for checking if a user is already logged in and for getting the logged in user. You can replace the following methods in all controllers and views:

Devise method name Rodauth method name
current_user rodauth.rails_account
user_signed_in? rodauth.logged_in?
new_user_registration_path rodauth.create_account_path
new_user_session_path rodauth.login_path
destroy_user_session_path rodauth.logout_path

You can see all routes with the rails rodauth:routes command.

Also note that the logout button can no longer use the DELETE method, but must now use POST.

Authentication check

The only remaining modification concerns the call to authenticate_user!.

Depending on how you used Devise, one of three approaches is easier:

  • If the routes requiring authentication are all under a small number of path prefixes (like /admin and /profile), adopt the Rodauth way and handle your authentication in a middleware.
  • If you had multiple authenticate_user! before_actions spread through your controllers, go to blacklist.
  • If you had a single authenticate_user! before_action in ApplicationController, that you skipped with skip_before_action :authenticate_user! for those controllers that didn’t need authentication, go to whitelist.

Middleware

Open the middleware in app/misc/rodauth_app.rb and authenticate your requets by defining a path prefix:

if r.path.start_with?("/admin") || r.path.start_with?("/profile")
  rodauth.require_account
end

Then, remove all calls to authenticate_user! that are still present in your controllers. Make sure all routes are now covered by the middleware!

class ProfileController < ApplicationController
-  before_action :authenticate_user!

  ...
end

Blacklisting

Add the authenticate_user! method to your ApplicationController

class ApplicationController < ActionController::Base
  before_filter :authenticate_user!

  private

+  def authenticate_user!
+    rodauth.require_account
+  end
end

For each controller you want to protect, you should already have a before action. There is no need for further changes.

class BackendController < ApplicationController
  before_action :authenticate_user!

  ...
end

Whitelisting

Add the authenticate_user! method to your ApplicationController. You should already have a before_filter that you can now reuse.

class ApplicationController < ActionController::Base
  before_filter :authenticate_user!

  private

+ def authenticate_user!
+   if controller_name != 'rodauth'
+     rodauth.require_account
+   end
+ end

Verification

By default, Rodauth accounts are unverified. If you want to verify them (because they usually are if you migrate from Devise), run the following in a Rails console:

User.find_each do |user|
  user.update!(status: :verified)
end

Cleanup

This should be enough to migrate basic applications. Make sure you check the Rodauth documentation for any configuration changes that might be necessary.