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
androdauth_main.rb
inapp/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 inApplicationController
, that you skipped withskip_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.