Rails Authentication with Rodauth
In this tutorial, we’ll show how to add fully functional authentication and account management functionality into a Rails app, using the Rodauth authentication framework. Rodauth has many advantages over the mainstream alternatives such as Devise, Sorcery, Clearance, and Authlogic, see my previous article for an introduction.
We’ll be working with a fresh Rails app using PostgresSQL, Hotwire, Bootstrap, home page, navbar, flash messages, and posts scaffold setup.
$ rails new blog --database=postgresql --css=bootstrap
$ cd blog
$ rails db:create
$ rails generate controller home index
$ rails generate scaffold post title:string body:text
$ rails db:migrate
# config/routes.rb
Rails.application.routes.draw do
root to: "home#index"
resources :posts
end
<!-- app/views/layouts/application.html.erb -->
<!-- ... -->
<body>
<%= render "navbar" %>
<div class="container">
<%= render "flash" %>
<%= yield %>
</div>
</body>
<!-- ... -->
<!-- app/views/application/_flash.html.erb -->
<% if notice %>
<div class="alert alert-success"><%= notice %></div>
<% end %>
<% if alert %>
<div class="alert alert-danger"><%= alert %></div>
<% end %>
<!-- app/views/application/_navbar.html.erb -->
<nav class="navbar navbar-expand-sm navbar-light bg-light border-bottom mb-4">
<div class="container">
<%= link_to "Rails App", root_path, class: "navbar-brand" %>
<div class="navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item">
<%= link_to "Posts", posts_path, class: "nav-link #{"active" if request.path.start_with?("/posts")}" %>
</li>
</ul>
</div>
</div>
</nav>
Installing Rodauth
Let’s start by adding the rodauth-rails gem to our Gemfile:
$ bundle add rodauth-rails
Next, we’ll run the rodauth:install
generator provided by rodauth-rails:
$ rails generate rodauth:install
# create db/migrate/20200820215819_create_rodauth.rb
# create config/initializers/rodauth.rb
# create config/initializers/sequel.rb
# create app/misc/rodauth_app.rb
# create app/misc/rodauth_main.rb
# create app/controllers/rodauth_controller.rb
# create app/models/account.rb
# create app/mailers/rodauth_mailer.rb
This will create the Rodauth app and some default Rodauth configuration, configure Sequel which Rodauth uses for database interaction to reuse Active Record’s database connection, and generate a migration that will create tables for the loaded Rodauth features. Let’s run the migration:
$ rails db:migrate
# == CreateRodauth: migrating ==========================
# -- create_table(:accounts)
# -- create_table(:account_password_hashes)
# -- create_table(:account_password_reset_keys)
# -- create_table(:account_verification_keys)
# -- create_table(:account_login_change_keys)
# -- create_table(:account_remember_keys)
# == CreateRodauth: migrated ===========================
We’ll also need to set Action Mailer’s default URL options for Rodauth to be
able to generate email links in RodauthMailer
:
# config/environments/development.rb
Rails.application.configure do
# ...
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end
After restarting the Rails server, we should be able to open the
/create-account
page and see Rodauth’s default registration form.
Adding authentication links
Rodauth configuration generated by rodauth-rails provides several routes for authentication and account management:
$ rails rodauth:routes
# /login rodauth.login_path
# /create-account rodauth.create_account_path
# /verify-account-resend rodauth.verify_account_resend_path
# /verify-account rodauth.verify_account_path
# /logout rodauth.logout_path
# /remember rodauth.remember_path
# /reset-password-request rodauth.reset_password_request_path
# /reset-password rodauth.reset_password_path
# /change-password rodauth.change_password_path
# /change-login rodauth.change_login_path
# /verify-login-change rodauth.verify_login_change_path
# /close-account rodauth.close_account_path
Let’s use this information to add some main authentication links to our navbar:
<!-- app/views/application/_navbar.html.erb -->
<!-- ... --->
<% if rodauth.logged_in? %>
<div class="dropdown">
<button class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" type="button">
<%= current_account.email %>
</button>
<div class="dropdown-menu dropdown-menu-end">
<%= link_to "Change password", rodauth.change_password_path, class: "dropdown-item" %>
<%= link_to "Change email", rodauth.change_login_path, class: "dropdown-item" %>
<div class="dropdown-divider"></div>
<%= link_to "Close account", rodauth.close_account_path, class: "dropdown-item text-danger" %>
<%= link_to "Sign out", rodauth.logout_path, data: { turbo_method: :post }, class: "dropdown-item" %>
</div>
</div>
<% else %>
<div>
<%= link_to "Sign in", rodauth.login_path, class: "btn btn-outline-primary" %>
<%= link_to "Sign up", rodauth.create_account_path, class: "btn btn-success" %>
</div>
<% end %>
<!-- ... --->
Here we’re using the #current_account
helper method that rodauth-rails
provides, which returns the currently signed in account.
Now our application will show login and registration links when the user is not logged in:
While logged in users will see some basic account management links:
Requiring authentication
Now that we have working authentication, we’ll likely want to require the user to be authenticated for certain parts of our application. In our case, we want to authenticate the posts controller.
We could add a before_action
callback to the controller, but Rodauth allows
us to do this inside the Rodauth app’s route block, which is called before each
Rails route. This way we can keep our authentication logic contained in a
single place.
# app/misc/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
# ...
route do |r|
# ...
if r.path.start_with?("/posts")
rodauth.require_authentication
end
end
end
Now visiting the /posts
page will redirect the user to the /login
page if
they’re not logged in.
We’ll also want to associate the posts to the accounts
table:
$ rails generate migration add_account_id_to_posts account:references
$ rails db:migrate
# app/models/account.rb
class Account < ApplicationRecord
# ...
has_many :posts
end
And scope them to the current account in the posts controller:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def index
@posts = current_account.posts.all
end
# ...
def create
@post = current_account.posts.build(post_params)
# ...
end
# ...
private
def set_post
@post = current_account.posts.find(params[:id])
end
# ...
end
Adding new fields
To have something other than an email address to display our users, let’s require users to enter their name during registration. This will also give us an opportunity to see how Rodauth can be configured.
Since we’ll need to edit the registration form, let’s first copy Rodauth’s HTML templates into our Rails application:
$ rails generate rodauth:views
# create app/views/rodauth/_login_form.html.erb
# create app/views/rodauth/_login_form_footer.html.erb
# create app/views/rodauth/_login_form_header.html.erb
# create app/views/rodauth/login.html.erb
# create app/views/rodauth/multi_phase_login.html.erb
# create app/views/rodauth/logout.html.erb
# create app/views/rodauth/create_account.html.erb
# create app/views/rodauth/verify_account_resend.html.erb
# create app/views/rodauth/verify_account.html.erb
# create app/views/rodauth/reset_password_request.html.erb
# create app/views/rodauth/reset_password.html.erb
# create app/views/rodauth/change_password.html.erb
# create app/views/rodauth/change_login.html.erb
# create app/views/rodauth/close_account.html.erb
We can now open the create_account.erb
template and add a new name
field:
<!-- app/views/rodauth/create_account.erb -->
<%= form_with url: rodauth.create_account_path, method: :post, data: { turbo: false } do |form| %>
<!-- new "name" field -->
<div class="mb-3">
<%= form.label :name, "Name", class: "form-label" %>
<%= form.text_field :name, value: params[:name], required: true, class: "form-control #{"is-invalid" if rodauth.field_error("name")}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error("name")) %>
<%= content_tag(:span, rodauth.field_error("name"), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error("name") %>
</div>
<!-- ... -->
<% end %>
Since the user’s name won’t be used for authentication, let’s store it in a new
profiles
table, and associate the profiles
table to the accounts
table.
$ rails generate model Profile account:references name:string
$ rails db:migrate
# app/models/account.rb
class Account < ApplicationRecord
# ...
has_one :profile
end
We now need our Rodauth app to actually handle the new name
parameter. We’ll
validate that it’s filled in and create the associated profile record after the
account is created.
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
before_create_account do
# Validate presence of the name field
throw_error_status(422, "name", "must be present") unless param_or_nil("name")
end
after_create_account do
# Create the associated profile record with name
Profile.create!(account_id: account_id, name: param("name"))
end
after_close_account do
# Delete the associated profile record
Profile.find_by!(account_id: account_id).destroy
end
# ...
end
end
Now we can update our navbar to use the user’s name instead of their email address:
<button class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" type="button">
- <%= current_account.email %>
+ <%= current_account.profile.name %>
</button>
Closing words
In this tutorial we’ve gradually built out a complete authentication and account management flow using the Rodauth authentication framework. It supports login & logout, account creation with email verification and a grace period, password change & password reset, email change with email verification, and close account functionality. We’ve seen how to add authentication links, require authentication for certain routes, and add new fields to the registration form.
I’m personally very excited about Rodauth, as it has an impressive featureset and a refreshingly clean design, and also it’s not tied to Rails. I’ve been working hard on rodauth-rails to make it as easy as possible to get started with in Rails, so hopefully it will help Rodauth gain more traction in the Rails community.