rodauth-rails
Provides Rails integration for the Rodauth authentication framework.
Resources
Useful links:
Articles:
- Rodauth: A Refreshing Authentication Solution for Ruby
- Adding Authentication in Rails 6 with Rodauth
Upgrading
Upgrading to 0.7.0
Starting from version 0.7.0, rodauth-rails now correctly detects Rails
application's secret_key_base when setting default hmac_secret, including
when it's set via credentials or $SECRET_KEY_BASE environment variable. This
means authentication will be more secure by default, and Rodauth features that
require hmac_secret should now work automatically as well.
However, if you've already been using rodauth-rails in production, where the
secret_key_base is set via credentials or environment variable and hmac_secret
was not explicitly set, the fact that your authentication will now start using
HMACs has backwards compatibility considerations. See the Rodauth
documentation for instructions on how to safely transition, or just set
hmac_secret nil in your Rodauth configuration.
Installation
Add the gem to your Gemfile:
gem "rodauth-rails", "~> 0.6"
# gem "jwt", require: false # for JWT feature
# gem "rotp", require: false # for OTP feature
# gem "rqrcode", require: false # for OTP feature
# gem "webauthn", require: false # for WebAuthn feature
Then run bundle install.
Next, run the install generator:
$ rails generate rodauth:install
The generator will create the following files:
- Rodauth migration at
db/migrate/*_create_rodauth.rb - Rodauth initializer at
config/initializers/rodauth.rb - Sequel initializer at
config/initializers/sequel.rbfor ActiveRecord integration - Rodauth app at
app/lib/rodauth_app.rb - Rodauth controller at
app/controllers/rodauth_controller.rb - Account model at
app/models/account.rb
Migration
The migration file creates tables required by Rodauth. You're encouraged to review the migration, and modify it to only create tables for features you intend to use.
# db/migrate/*_create_rodauth.rb
class CreateRodauth < ActiveRecord::Migration
def change
create_table :accounts do |t| ... end
create_table :account_password_hashes do |t| ... end
create_table :account_password_reset_keys do |t| ... end
create_table :account_verification_keys do |t| ... end
create_table :account_login_change_keys do |t| ... end
create_table :account_remember_keys do |t| ... end
end
end
Once you're done, you can run the migration:
$ rails db:migrate
Rodauth initializer
The Rodauth initializer assigns the constant for your Rodauth app, which will be called by the Rack middleware that's added in front of your Rails router.
# config/initializers/rodauth.rb
Rodauth::Rails.configure do |config|
config.app = "RodauthApp"
end
Sequel initializer
Rodauth uses Sequel for database interaction. If you're using ActiveRecord, an additional initializer will be created which configures Sequel to use the ActiveRecord connection.
# config/initializers/sequel.rb
require "sequel/core"
# initialize Sequel and have it reuse Active Record's database connection
DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
Rodauth app
Your Rodauth app is created in the app/lib/ directory, and comes with a
default set of authentication features enabled, as well as extensive examples
on ways you can configure authentication behaviour.
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
configure do
# authentication configuration
end
route do |r|
# request handling
end
end
Controller
Your Rodauth app will by default use RodauthController for view rendering,
CSRF protection, and running controller callbacks and rescue handlers around
Rodauth actions.
# app/controllers/rodauth_controller.rb
class RodauthController < ApplicationController
end
Account model
Rodauth stores user accounts in the accounts table, so the generator will
also create an Account model for custom use.
# app/models/account.rb
class Account < ApplicationRecord
end
Usage
Routes
We can see the list of routes our Rodauth middleware handles:
$ rails rodauth:routes
Routes handled by RodauthApp:
/login rodauth.login_path
/create-account rodauth.create_account_path
/verify-account-resend rodauth.verify_account_resend_path
/verify-account rodauth.verify_account_path
/change-password rodauth.change_password_path
/change-login rodauth.change_login_path
/logout rodauth.logout_path
/remember rodauth.remember_path
/reset-password-request rodauth.reset_password_request_path
/reset-password rodauth.reset_password_path
/verify-login-change rodauth.verify_login_change_path
/close-account rodauth.close_account_path
Using this information, we could add some basic authentication links to our navigation header:
<ul>
<% if rodauth.authenticated? %>
<li><%= link_to "Sign out", rodauth.logout_path, method: :post %></li>
<% else %>
<li><%= link_to "Sign in", rodauth.login_path %></li>
<li><%= link_to "Sign up", rodauth.create_account_path %></li>
<% end %>
</ul>
These routes are fully functional, feel free to visit them and interact with the pages. The templates that ship with Rodauth aim to provide a complete authentication experience, and the forms use Bootstrap markup.
Current account
To be able to fetch currently authenticated account, let's define a
#current_account method that fetches the account id from session and
retrieves the corresponding account record:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :current_account, if: -> { rodauth.authenticated? }
private
def current_account
@current_account ||= Account.find(rodauth.session_value)
rescue ActiveRecord::RecordNotFound
rodauth.logout
rodauth.login_required
end
helper_method :current_account
end
This allows us to access the current account in controllers and views:
<p>Authenticated as: <%= current_account.email %></p>
Requiring authentication
We'll likely want to require authentication for certain parts of our app, redirecting the user to the login page if they're not logged in. We can do this in our Rodauth app's routing block, which helps keep the authentication logic encapsulated:
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
# ...
route do |r|
# ...
r.rodauth # route rodauth requests
# require authentication for /dashboard/* and /account/* routes
if r.path.start_with?("/dashboard") || r.path.start_with?("/account")
rodauth.require_authentication # redirect to login page if not authenticated
end
end
end
We can also require authentication at the controller layer:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
private
def authenticate
rodauth.require_authentication # redirect to login page if not authenticated
end
end
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
before_action :authenticate
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate, except: [:index, :show]
end
Or at the Rails router level:
# config/routes.rb
Rails.application.routes.draw do
constraints -> (r) { r.env["rodauth"].require_authentication } do
namespace :admin do
# ...
end
end
end
Views
The templates built into Rodauth are useful when getting started, but soon you'll want to start editing the markup. You can run the following command to copy Rodauth templates into your Rails app:
$ rails generate rodauth:views
This will generate views for the default set of Rodauth features into the
app/views/rodauth directory, which will be automatically picked up by the
RodauthController.
You can pass a list of Rodauth features to the generator to create views for these features (this will not remove any existing views):
$ rails generate rodauth:views login create_account lockout otp
Or you can generate views for all features:
$ rails generate rodauth:views --all
You can also tell the generator to create views into another directory (in this case make sure to rename the Rodauth controller accordingly):
# generates views into app/views/authentication
$ rails generate rodauth:views --name authentication
Layout
To use different layouts for different Rodauth views, you can compare the request path in the layout method:
class RodauthController < ApplicationController
layout :rodauth_layout
private
def rodauth_layout
case request.path
when rodauth.login_path,
rodauth.create_account_path,
rodauth.verify_account_path,
rodauth.reset_password_path,
rodauth.reset_password_request_path
"authentication"
else
"dashboard"
end
end
end
Mailer
Depending on the features you've enabled, Rodauth may send emails as part of the authentication flow. Most email settings can be customized:
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
# ...
configure do
# ...
# general settings
email_from "[email protected]"
email_subject_prefix "[MyApp] "
send_email(&:deliver_later)
# ...
# feature settings
verify_account_email_subject "Verify your account"
verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
# ...
end
end
This is convenient when starting out, but eventually you might want to use your own mailer. You can start by running the following command:
$ rails generate rodauth:mailer
This will create a RodauthMailer with the associated mailer views in
app/views/rodauth_mailer directory.
# app/mailers/rodauth_mailer.rb
class RodauthMailer < ApplicationMailer
def verify_account(recipient, email_link) ... end
def reset_password(recipient, email_link) ... end
def verify_login_change(recipient, old_login, new_login, email_link) ... end
def password_changed(recipient) ... end
# def email_auth(recipient, email_link) ... end
# def unlock_account(recipient, email_link) ... end
end
You can then uncomment the lines in your Rodauth configuration to have it call
your mailer. If you've enabled additional authentication features that send
emails, make sure to override their create_*_email methods as well.
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
# ...
configure do
# ...
create_reset_password_email do
RodauthMailer.reset_password(email_to, reset_password_email_link)
end
create_verify_account_email do
RodauthMailer.verify_account(email_to, verify_account_email_link)
end
create_verify_login_change_email do |login|
RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
end
create_password_changed_email do
RodauthMailer.password_changed(email_to)
end
# create_email_auth_email do
# RodauthMailer.email_auth(email_to, email_auth_email_link)
# end
# create_unlock_account_email do
# RodauthMailer.unlock_account(email_to, unlock_account_email_link)
# end
send_email do |email|
# queue email delivery on the mailer after the transaction commits
db.after_commit { email.deliver_later }
end
# ...
end
end
This approach can be used even if you're using a 3rd-party service for
transactional emails, where emails are sent via API requests instead of
SMTP. Whatever the create_*_email block returns will be passed to
send_email, so you can be creative.
Migrations
The install generator will create a migration for tables used by the Rodauth features enabled by default. For any additional features, you can use the migration generator to create the corresponding tables:
$ rails generate rodauth:migration otp sms_codes recovery_codes
# db/migration/*_create_rodauth_otp_sms_codes_recovery_codes.rb
class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
def change
create_table :account_otp_keys do |t| ... end
create_table :account_sms_codes do |t| ... end
create_table :account_recovery_codes do |t| ... end
end
end
JSON API
JSON API support in Rodauth is provided by the JWT feature. First you'll need to add the JWT gem to your Gemfile:
gem "jwt"
The following configuration will enable the Rodauth endpoints to be accessed via JSON requests (in addition to HTML requests):
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
configure(json: true) do
# ...
enable :jwt
jwt_secret "...your secret key..."
# ...
end
end
If you want the endpoints to be only accessible via JSON requests, or if your
Rails app is in API-only mode, instead of json: true pass json: :only to
the configure method.
Make sure to store the jwt_secret in a secure place, such as Rails
credentials or environment variables.
Calling controller methods
When using Rodauth before/after hooks or generally overriding your Rodauth
configuration, in some cases you might want to call methods defined on your
controllers. You can do so with rails_controller_eval, for example:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
private
def setup_tracking(account_id)
# ... some implementation ...
end
end
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
configure do
after_create_account do
rails_controller_eval { setup_tracking(account_id) }
end
end
end
Rodauth instance
In some cases you might need to use Rodauth more programmatically, and perform Rodauth operations outside of the request context. rodauth-rails gives you the ability to retrieve the Rodauth instance:
rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
rodauth.login_url #=> "https://example.com/login"
rodauth.account_from_login("[email protected]") # loads user by email
rodauth.password_match?("secret") #=> true
rodauth.setup_account_verification
rodauth.close_account
This Rodauth instance will be initialized with basic Rack env that allows is it
to generate URLs, using config.action_mailer.default_url_options options.
How it works
Middleware
rodauth-rails inserts a Rodauth::Rails::Middleware into your middleware
stack, which calls your Rodauth app for each request, before the request
reaches the Rails router.
$ rails middleware
...
use Rodauth::Rails::Middleware
run MyApp::Application.routes
The Rodauth app stores the Rodauth::Auth instance in the Rack env hash, which
is then available in your Rails app:
request.env["rodauth"] #=> #<Rodauth::Auth>
request.env["rodauth.secondary"] #=> #<Rodauth::Auth> (if using multiple configurations)
For convenience, this object can be accessed via the #rodauth method in views
and controllers:
class MyController < ApplicationController
def my_action
rodauth #=> #<Rodauth::Auth>
rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations)
end
end
<% rodauth #=> #<Rodauth::Auth> %>
<% rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations) %>
App
The Rodauth::Rails::App class is a Roda subclass that provides Rails
integration for Rodauth:
- uses Action Dispatch flash instead of Roda's
- uses Action Dispatch CSRF protection instead of Roda's
- sets HMAC secret to Rails' secret key base
- uses Action Controller for rendering templates
- runs Action Controller callbacks & rescue handlers around Rodauth actions
- uses Action Mailer for sending emails
The configure { ... } method wraps configuring the Rodauth plugin, forwarding
any additional plugin options.
configure { ... } # defining default Rodauth configuration
configure(json: true) { ... } # passing options to the Rodauth plugin
configure(:secondary) { ... } # defining multiple Rodauth configurations
Sequel
Rodauth uses the Sequel library for database queries, due to more advanced database usage (SQL expressions, database-agnostic date arithmetic, SQL function calls).
If ActiveRecord is used in the application, the rodauth:install generator
will have automatically configured Sequel to reuse ActiveRecord's database
connection (using the sequel-activerecord_connection gem).
This means that, from the usage perspective, Sequel can be considered just as an implementation detail of Rodauth.
Configuring
For the list of configuration methods provided by Rodauth, see the feature documentation.
The rails feature rodauth-rails loads is customizable as well, here is the
list of its configuration methods:
| Name | Description |
|---|---|
rails_render(**options) |
Renders the template with given render options. |
rails_csrf_tag |
Hidden field added to Rodauth templates containing the CSRF token. |
rails_csrf_param |
Value of the name attribute for the CSRF tag. |
rails_csrf_token |
Value of the value attribute for the CSRF tag. |
rails_check_csrf! |
Verifies the authenticity token for the current request. |
rails_controller_instance |
Instance of the controller with the request env context. |
rails_controller |
Controller class to use for rendering and CSRF protection. |
The Rodauth::Rails module has a few config settings available as well:
| Name | Description |
|---|---|
app |
Constant name of your Rodauth app, which is called by the middleware. |
middleware |
Whether to insert the middleware into the Rails application's middleware stack. Defaults to true. |
# config/initializers/rodauth.rb
Rodauth::Rails.configure do |config|
config.app = "RodauthApp"
config.middleware = true
end
Testing
If you're writing system tests, it's generally better to go through the actual authentication flow with tools like Capybara, and to not use any stubbing.
In functional and integration tests you can just make requests to Rodauth routes:
# test/controllers/posts_controller_test.rb
class PostsControllerTest < ActionDispatch::IntegrationTest
test "should require authentication" do
get posts_url
assert_redirected_to "/login"
login
get posts_url
assert_response :success
logout
assert_redirected_to "/login"
end
private
def login(login: "[email protected]", password: "secret")
post "/create-account", params: {
"login" => login,
"password" => password,
"password-confirm" => password,
}
post "/login", params: {
"login" => login,
"password" => password,
}
end
def logout
post "/logout"
end
end
Rodauth defaults
rodauth-rails changes some of the default Rodauth settings for easier setup:
Database functions
By default, on PostgreSQL, MySQL, and Microsoft SQL Server Rodauth uses database functions to access password hashes, with the user running the application unable to get direct access to password hashes. This reduces the risk of an attacker being able to access password hashes and use them to attack other sites.
While this is useful additional security, it is also more complex to set up and to reason about, as it requires having two different database users and making sure the correct migration is run for the correct user.
To keep with Rails' "convention over configuration" doctrine, rodauth-rails disables the use of database functions, though you can always turn it back on.
use_database_authentication_functions? true
To create the database functions, pass the Sequel database object into the Rodauth method for creating database functions:
# db/migrate/*_create_rodauth_database_functions.rb
class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
def up
Rodauth.create_database_authentication_functions(DB)
end
def down
Rodauth.drop_database_authentication_functions(DB)
end
end
Account statuses
The recommended Rodauth migration stores possible account status values in a separate table, and creates a foreign key on the accounts table, which ensures only a valid status value will be persisted.
Unfortunately, this doesn't work when the database is restored from the schema file, in which case the account statuses table will be empty. This happens in tests by default, but it's also commonly done in development.
To address this, rodauth-rails modifies the setup to store account status text directly in the accounts table. If you're worried about invalid status values creeping in, you may use enums instead. Alternatively, you can always go back to the setup recommended by Rodauth.
# in the migration:
create_table :account_statuses do |t|
t.string :name, null: false, unique: true
end
execute "INSERT INTO account_statuses (id, name) VALUES (1, 'Unverified'), (2, 'Verified'), (3, 'Closed')"
create_table :accounts do |t|
# ...
t.references :status, foreign_key: { to_table: :account_statuses }, null: false, default: 1
# ...
end
configure do
# ...
- account_status_column :status
- account_unverified_status_value "unverified"
- account_open_status_value "verified"
- account_closed_status_value "closed"
# ...
end
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the rodauth-rails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.