Need help with rodauth-rails?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

janko
199 Stars 11 Forks MIT License 287 Commits 2 Opened issues

Description

Rails integration for Rodauth authentication framework

Services available

!
?

Need anything else?

Contributors list

# 16,012
Ruby
imagema...
s3
gridfs
289 commits
# 102,844
Ruby
omniaut...
Rails
Shell
3 commits
# 206,152
Ruby
HTML
chromiu...
stimulu...
1 commit
# 229,553
HTML
Rails
Shell
C
1 commit
# 38,106
Rails
React
preact-...
html5
1 commit
# 265,030
HTML
CSS
Rails
Shell
1 commit

rodauth-rails

Provides Rails integration for the Rodauth authentication framework.

Resources

Useful links:

Articles:

Why Rodauth?

There are already several popular authentication solutions for Rails (Devise, Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Here are some of the advantages that stand out for me:

One commmon concern is the fact that, unlike most other authentication frameworks for Rails, Rodauth uses Sequel for database interaction instead of Active Record. There are good reasons for this, and to make Rodauth work smoothly alongside Active Record, rodauth-rails configures Sequel to reuse Active Record's database connection.

Upgrading

For instructions on upgrading from previous rodauth-rails versions, see UPGRADING.md.

Installation

Add the gem to your Gemfile:

gem "rodauth-rails", "~> 0.17"

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

Or if you want Rodauth endpoints to be exposed via JSON API:

$ rails generate rodauth:install --json # regular authentication using the Rails session
# or
$ rails generate rodauth:install --jwt # token authentication via the "Authorization" header
$ bundle add jwt

This generator will create a Rodauth app with common authentication features enabled, a database migration with tables required by those features, a mailer with default templates, and a few other files.

Feel free to remove any features you don't need, along with their corresponding tables. Afterwards, run the migration:

$ rails db:migrate

Usage

Routes

You 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, you can add some basic authentication links to your navigation header:


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.

Inside Rodauth configuration and the

route
block you can access Rails route helpers through
#rails_routes
:
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    login_redirect { rails_routes.activity_path }
    # ...
  end
end

Current account

The

#current_account
method is defined in controllers and views, which returns the model instance of the currently logged in account.
current_account #=> #
current_account.email #=> "[email protected]"

If the account doesn't exist in the database, the session will be cleared and login required.

Pass the configuration name to retrieve accounts belonging to other Rodauth configurations:

current_account(:admin)

The

#current_account
method will try to infer the account model class from the configured table name. If that fails, you can set the account model manually:
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    rails_account_model Authentication::Account # custom model name
  end
end

Requiring authentication

You'll likely want to require authentication for certain parts of your app, redirecting the user to the login page if they're not logged in. You can do this in your 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

You 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

Routing constraints

In some cases it makes sense to require authentication at the Rails router level. You can do this via the built-in

authenticated
routing constraint:
# config/routes.rb
Rails.application.routes.draw do
  constraints Rodauth::Rails.authenticated do
    # ... authenticated routes ...
  end
end

If you want additional conditions, you can pass in a block, which is called with the Rodauth instance:

# config/routes.rb
Rails.application.routes.draw do
  # require multifactor authentication to be setup
  constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
    # ...
  end
end

You can specify the Rodauth configuration by passing the configuration name:

# config/routes.rb
Rails.application.routes.draw do
  constraints Rodauth::Rails.authenticated(:admin) do
    # ...
  end
end

If you need something more custom, you can always create the routing constraint manually:

# config/routes.rb
Rails.application.routes.draw do
  constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
    # routes when the user is not logged in
  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, provided that
RodauthController
is set for the main configuration.

You can pass a list of Rodauth features to the generator to create views for these features (this will not remove or overwrite 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

Use

--name
to generate views for a different Rodauth configuration:
$ rails generate rodauth:views --name admin

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

The install generator will create

RodauthMailer
with default email templates, and configure Rodauth features that send emails as part of the authentication flow to use it.
# 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
# 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 configuration calls

#deliver_later
, which uses Active Job to deliver emails in a background job. It's generally recommended to send emails asynchronously for better request throughput and the ability to retry deliveries. However, if you want to send emails synchronously, you can modify the configuration to call
#deliver_now
instead.

If you're using a background processing library without an Active Job adapter, or a 3rd-party service for sending transactional emails, this two-phase API might not be suitable. In this case, instead of overriding

#create_*_email
and
#send_email
, override the
#send_*_email
methods instead, which are required to send the email immediately. For example:
# app/workers/rodauth_mailer_worker.rb
class RodauthMailerWorker
  include Sidekiq::Worker

def perform(name, *args) email = RodauthMailer.public_send(name, *args) email.deliver_now end end

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    # use #send_*_email method to be able to immediately enqueue email delivery
    send_reset_password_email do
      enqueue_email(:reset_password, email_to, reset_password_email_link)
    end
    # ...
    auth_class_eval do
      # custom method for enqueuing email delivery using our worker
      def enqueue_email(name, *args)
        db.after_commit do
          RodauthMailerWorker.perform_async(name, *args)
        end
      end
    end
    # ...
  end
end

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

Model

The

Rodauth::Rails::Model
mixin can be included into the account model, which defines a password attribute and associations for tables used by enabled authentication features.
class Account < ApplicationRecord
  include Rodauth::Rails.model # or `Rodauth::Rails.model(:admin)`
end

Password attribute

Regardless of whether you're storing the password hash in a column in the accounts table, or in a separate table, the

#password
attribute can be used to set or clear the password hash.
account = Account.create!(email: "[email protected]", password: "secret")

when password hash is stored in a column on the accounts table

account.password_hash #=> "$2a$12$k/Ub1I2iomi84RacqY89Hu4.M0vK7klRnRtzorDyvOkVI.hKhkNw."

when password hash is stored in a separate table

account.password_hash #=> #<:passwordhash...> (record from account_password_hashes table) account.password_hash.password_hash #=> "$2a$12$k/Ub1..." (inaccessible when using database authentication functions)

account.password = nil # clears password hash account.password_hash #=> nil </:passwordhash...>

Note that the password attribute doesn't come with validations, making it unsuitable for forms. It was primarily intended to allow easily creating accounts in development console and in tests.

Associations

The

Rodauth::Rails::Model
mixin defines associations for Rodauth tables associated to the accounts table:
account.remember_key #=> #<:rememberkey> (record from `account_remember_keys` table)
account.active_session_keys #=> [#<:activesessionkey>,...] (records from `account_active_session_keys` table)

You can also reference the associated models directly:

# model referencing the `account_authentication_audit_logs` table
Account::AuthenticationAuditLog.where(message: "login").group(:account_id)

The associated models define the inverse

belongs_to :account
association:
Account::ActiveSessionKey.includes(:account).map(&:account)

Here is an example of using associations to create a method that returns whether the account has multifactor authentication enabled:

class Account < ApplicationRecord
  include Rodauth::Rails.model

def mfa_enabled? otp_key || (sms_code && sms_code.num_failures.nil?) || recovery_codes.any? end end

Here is another example of creating a query scope that selects accounts with multifactor authentication enabled:

class Account < ApplicationRecord
  include Rodauth::Rails.model

scope :otp_setup, -> { where(otp_key: OtpKey.all) } scope :sms_codes_setup, -> { where(sms_code: SmsCode.where(num_failures: nil)) } scope :recovery_codes_setup, -> { where(recovery_codes: RecoveryCode.all) } scope :mfa_enabled, -> { merge(otp_setup.or(sms_codes_setup).or(recovery_codes_setup)) } end

Below is a list of all associations defined depending on the features loaded:

| Feature | Association | Type | Model | Table (default) | | :------ | :---------- | :--- | :---- | :---- | | accountexpiration | `:activitytime

|
hasone
|
ActivityTime
|
account
activitytimes` | | activesessions |
:active_session_keys
|
has_many
|
ActiveSessionKey
|
account_active_session_keys
| | auditlogging | `:authenticationauditlogs
|
has
many
|
AuthenticationAuditLog
|
accountauthenticationauditlogs` | | disallowpasswordreuse | `:previouspasswordhashes
|
has
many
|
PreviousPasswordHash
|
accountpreviouspasswordhashes` | | emailauth |
:email_auth_key
|
has_one
|
EmailAuthKey
|
account_email_auth_keys
| | jwtrefresh | `:jwtrefreshkeys
|
has
many
|
JwtRefreshKey
|
accountjwtrefreshkeys
|
| lockout                 |
:lockout
|
has
one
|
Lockout
|
accountlockouts
|
| lockout                 |
:login
failure
|
hasone
|
LoginFailure
|
account
loginfailures
|
| otp                     |
:otp
key
|
hasone
|
OtpKey
|
account
otpkeys` | | passwordexpiration |
:password_change_time
|
has_one
|
PasswordChangeTime
|
account_password_change_times
| | recoverycodes | `:recoverycodes
|
hasmany
|
RecoveryCode
|
account
recoverycodes
|
| remember                |
:remember
key
|
hasone
|
RememberKey
|
account
rememberkeys` | | resetpassword |
:password_reset_key
|
has_one
|
PasswordResetKey
|
account_password_reset_keys
| | singlesession | `:sessionkey
|
hasone
|
SessionKey
|
account
sessionkeys` | | smscodes |
:sms_code
|
has_one
|
SmsCode
|
account_sms_codes
| | verifyaccount | `:verificationkey
|
hasone
|
VerificationKey
|
account
verificationkeys` | | verifyloginchange | `:loginchangekey
|
has
one
|
LoginChangeKey
|
accountloginchangekeys
|
| webauthn                |
:webauthn
keys
|
hasmany
|
WebauthnKey
|
account
webauthnkeys
|
| webauthn                |
:webauthn
userid
|
has
one
|
WebauthnUserId
|
accountwebauthnuser_ids` |

By default, all associations except for audit logs have

dependent: :destroy
set, to allow for easy deletion of account records in the console. You can use
:association_options
to modify global or per-association options:
# don't auto-delete associations when account model is deleted
Rodauth::Rails.model(association_options: { dependent: nil })

require authentication audit logs to be eager loaded before retrieval

Rodauth::Rails.model(association_options: -> (name) { { strict_loading: true } if name == :authentication_audit_logs })

Note that some Rodauth tables use composite primary keys, which Active Record doesn't support out of the box. For associations to work properly, you might need to add the compositeprimarykeys gem to your Gemfile.

Multiple configurations

If you need to handle multiple types of accounts that require different authentication logic, you can create additional configurations for them:

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  # primary configuration
  configure do
    # ...
  end

alternative configuration

configure(:admin) do # ... enable features ... prefix "/admin" session_key_prefix "admin_" remember_cookie_key "_admin_remember" # if using remember feature # ... end

route do |r| r.rodauth

r.on "admin" do
  r.rodauth(:admin)
  break # allow routing of other /admin/* requests to continue to Rails
end

# ...

end end

Then in your application you can reference the secondary Rodauth instance:

rodauth(:admin).login_path #=> "/admin/login"

You'll likely want to save the information of which account belongs to which configuration to the database. One way would be to have a separate table that stores account types:

$ rails generate migration create_account_types
# db/migrate/*_create_account_types.rb
class CreateAccountTypes < ActiveRecord::Migration
  def change
    create_table :account_types do |t|
      t.references :account, foreign_key: { on_delete: :cascade }, null: false
      t.string :type, null: false
    end
  end
end
$ rails db:migrate

Then an entry would be inserted after account creation, and optionally whenever Rodauth retrieves accounts you could filter only those belonging to the current configuration:

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure(:admin) do
    # ...
    after_create_account do
      db[:account_types].insert(account_id: account_id, type: "admin")
    end
    auth_class_eval do
      def account_ds(*)
        super.join(:account_types, account_id: :id).where(type: "admin")
      end
    end
    # ...
  end
end

Named auth classes

A

configure
block inside
Rodauth::Rails::App
will internally create an anonymous
Rodauth::Auth
subclass, and register it under the given name. However, you can also define the auth classes explicitly, by creating subclasses of
Rodauth::Rails::Auth
:
# app/lib/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ... main configuration ...
  end
end
# app/lib/rodauth_admin.rb
class RodauthAdmin < Rodauth::Rails::Auth
  configure do
    # ...
    prefix "/admin"
    session_key_prefix "admin_"
    # ...
  end
end
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure RodauthMain
  configure RodauthAdmin, :admin
  # ...
end

This allows having each configuration in a dedicated file, and named constants improve introspection and error messages. You can also use inheritance to share common settings:

# app/lib/rodauth_base.rb
class RodauthBase < Rodauth::Rails::Auth
  # common settings that can be shared between multiple configurations
  configure do
    enable :login, :logout
    login_return_to_requested_location? true
    logout_redirect "/"
    # ...
  end
end
# app/lib/rodauth_main.rb
class RodauthMain < RodauthBase # inherit common settings
  configure do
    # ... customize main ...
  end
end
# app/lib/rodauth_admin.rb
class RodauthAdmin < RodauthBase # inherit common settings
  configure do
    # ... customize admin ...
  end
end

Another benefit of explicit classes is that you can define custom methods directly at the class level instead of inside an

auth_class_eval
:
# app/lib/rodauth_admin.rb
class RodauthAdmin < Rodauth::Rails::Auth
  configure do
    # ...
  end

def superadmin? Role.where(account_id: session_id, type: "superadmin").any? end end

# config/routes.rb
Rails.application.routes.draw do
  constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
    mount Sidekiq::Web => "sidekiq"
  end
end

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

Outside of a request

In some cases you might need to use Rodauth more programmatically. If you want to perform authentication operations outside of request context, Rodauth ships with the internal_request feature just for that.

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    enable :internal_request
  end
end
# main configuration
RodauthApp.rodauth.create_account(login: "[email protected]", password: "secret")
RodauthApp.rodauth.verify_account(account_login: "[email protected]")

secondary configuration

RodauthApp.rodauth(:admin).close_account(account_login: "[email protected]")

The rodauth-rails gem additionally updates the internal rack env hash with your

config.action_mailer.default_url_options
, which is used for generating email links.

For generating authentication URLs outside of a request use the pathclassmethods plugin:

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    enable :path_class_methods
  end
end
# main configuration
RodauthApp.rodauth.create_account_path
RodauthApp.rodauth.verify_account_url(key: "abc123")

secondary configuration

RodauthApp.rodauth(:admin).close_account_path

Calling instance methods

If you need to access Rodauth methods not exposed as internal requests, you can use

Rodauth::Rails.rodauth
to retrieve the Rodauth instance used by the internal_request feature:
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    enable :internal_request # this is required
  end
end
account = Account.find_by!(email: "[email protected]")
rodauth = Rodauth::Rails.rodauth(account: account)

rodauth.compute_hmac("token") #=> "TpEJTKfKwqYvIDKWsuZhkhKlhaBXtR1aodskBAflD8U" rodauth.open_account? #=> true rodauth.two_factor_authentication_setup? #=> true rodauth.password_meets_requirements?("foo") #=> false rodauth.locked_out? #=> false

In addition to the

:account
option, the
Rodauth::Rails.rodauth
method accepts any options supported by the internal_request feature.
# main configuration
Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })

secondary configuration

Rodauth::Rails.rodauth(:admin, params: { "param" => "value" })

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"]       #=> #<:auth>
request.env["rodauth.admin"] #=> #<: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         #=> #<:auth>
    rodauth(:admin) #=> #<:auth> (if using multiple configurations)
  end
end
 #<:auth> %>
 #<: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.
class RodauthApp < Rodauth::Rails::App
  configure { ... }             # defining default Rodauth configuration
  configure(json: true) { ... } # passing options to the Rodauth plugin
  configure(:admin) { ... }     # defining multiple Rodauth configurations
end

The

route
block is provided by Roda, and it's called on each request before it reaches the Rails router.
class RodauthApp < Rodauth::Rails::App
  route do |r|
    # ... called before each request ...
  end
end

Since

Rodauth::Rails::App
is just a Roda subclass, you can do anything you would with a Roda app, such as loading additional Roda plugins:
class RodauthApp < Rodauth::Rails::App
  plugin :request_headers # easier access to request headers
  plugin :typecast_params # methods for conversion of request params
  plugin :default_headers, { "Foo" => "Bar" }
  # ...
end

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.

JSON API

To make Rodauth endpoints accessible via JSON API, enable the

json
feature:

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    enable :json
    only_json? true # accept only JSON requests (optional)
    # ...
  end
end

This will store account session data into the Rails session. If you rather want stateless token-based authentication via the

Authorization
header, enable the
jwt
feature (which builds on top of the
json
feature) and add the JWT gem to the Gemfile:
$ bundle add jwt
# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    enable :jwt
    jwt_secret "" # store the JWT secret in a safe place
    only_json? true # accept only JSON requests (optional)
    # ...
  end
end

The JWT token will be returned after each request to Rodauth routes. To also return the JWT token on requests to your app's routes, you can add the following code to your base controller:

class ApplicationController < ActionController::Base
  # ...
  after_action :set_jwt_token

private

def set_jwt_token if rodauth.use_jwt? && rodauth.valid_jwt? response.headers["Authorization"] = rodauth.session_jwt end end

...

end

OmniAuth

While Rodauth doesn't yet come with OmniAuth integration, we can build one ourselves using the existing Rodauth API.

In order to allow the user to login via multiple external providers, let's create an

account_identities
table that will have a many-to-one relationship with the
accounts
table:
$ rails generate model AccountIdentity
# db/migrate/*_create_account_identities.rb
class CreateAccountIdentities < ActiveRecord::Migration
  def change
    create_table :account_identities do |t|
      t.references :account, null: false, foreign_key: { on_delete: :cascade }
      t.string :provider, null: false
      t.string :uid, null: false
      t.jsonb :info, null: false, default: {} # adjust JSON column type for your database

  t.timestamps

  t.index [:provider, :uid], unique: true
end

end end

# app/models/account_identity.rb
class AcccountIdentity < ApplicationRecord
  belongs_to :account
end
# app/models/account.rb
class Account < ApplicationRecord
  has_many :identities, class_name: "AccountIdentity"
end

Let's assume we want to implement Facebook login, and have added the corresponding OmniAuth strategy to the middleware stack, together with an authorization link on the login form:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
    scope: "email", callback_path: "/auth/facebook/callback"
end

Finally, let's implement the OmniAuth callback endpoint on our Rodauth controller:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/auth/:provider/callback", to: "rodauth#omniauth"
end
# app/controllres/rodauth_controller.rb
class RodauthController < ApplicationController
  def omniauth
    auth = request.env["omniauth.auth"]

# attempt to find existing identity directly
identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])

if identity
  # update any external info changes
  identity.update!(info: auth["info"])
  # set account from identity
  account = identity.account
end

# attempt to find an existing account by email
account ||= Account.find_by(email: auth["info"]["email"])

# disallow login if account is not verified
if account &amp;&amp; account.status != rodauth.account_open_status_value
  redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
  return
end

# create new account if it doesn't exist
unless account
  account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
end

# create new identity if it doesn't exist
unless identity
  account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
end

# load the account into the rodauth instance
rodauth.account_from_login(account.email)

rodauth_response do # ensures any `after_action` callbacks get called
  # sign in the loaded account
  rodauth.login("omniauth")
end

end end

Configuring

The

rails
feature rodauth-rails loads provides the following 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. | |
rails_account_model
| Model class connected with the accounts table. |

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

For the list of configuration methods provided by Rodauth, see the feature documentation.

Custom extensions

When developing custom extensions for Rodauth inside your Rails project, it's probably better to use plain modules, at least in the beginning, as Rodauth feature design doesn't yet work well with Zeitwerk reloading.

Here is an example of an LDAP authentication extension that uses the simpleldapauthenticator gem.

# app/lib/rodauth_ldap.rb
module RodauthLdap
  def require_bcrypt?
    false
  end

def password_match?(password) SimpleLdapAuthenticator.valid?(account[:email], password) end end

# app/lib/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  configure do
    # ...
    auth_class_eval do
      include RodauthLdap
    end
    # ...
  end
end

Testing

System (browser) tests for Rodauth actions could look something like this:

# test/system/authentication_test.rb
require "test_helper"

class AuthenticationTest < ActionDispatch::SystemTestCase include ActiveJob::TestHelper driven_by :rack_test

test "creating and verifying an account" do create_account assert_match "An email has been sent to you with a link to verify your account", page.text

verify_account
assert_match "Your account has been verified", page.text

end

test "logging in and logging out" do create_account(verify: true)

logout
assert_match "You have been logged out", page.text

login
assert_match "You have been logged in", page.text

end

private

def create_account(email: "[email protected]", password: "secret", verify: false) visit "/create-account" fill_in "Login", with: email fill_in "Password", with: password fill_in "Confirm Password", with: password click_on "Create Account" verify_account if verify end

def verify_account perform_enqueued_jobs # run enqueued email deliveries email = ActionMailer::Base.deliveries.last verify_account_link = email.body.to_s[/\S+verify-account\S+/] visit verify_account_link click_on "Verify Account" end

def login(email: "[email protected]", password: "secret") visit "/login" fill_in "Login", with: email fill_in "Password", with: password click_on "Login" end

def logout visit "/logout" click_on "Logout" end end

While request tests in JSON API mode with JWT tokens could look something like this:

# test/integration/authentication_test.rb
require "test_helper"

class AuthenticationTest < ActionDispatch::IntegrationTest test "creating and verifying an account" do create_account assert_response :success assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]

verify_account
assert_response :success
assert_match "Your account has been verified", JSON.parse(body)["success"]

end

test "logging in and logging out" do create_account(verify: true)

logout
assert_response :success
assert_match "You have been logged out", JSON.parse(body)["success"]

login
assert_response :success
assert_match "You have been logged in", JSON.parse(body)["success"]

end

private

def create_account(email: "[email protected]", password: "secret", verify: false) post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password } verify_account if verify end

def verify_account perform_enqueued_jobs # run enqueued email deliveries email = ActionMailer::Base.deliveries.last verify_account_key = email.body.to_s[/verify-account?key=(\S+)/, 1] post "/verify-account", as: :json, params: { key: verify_account_key } end

def login(email: "[email protected]", password: "secret") post "/login", as: :json, params: { login: email, password: password } end

def logout post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] } end end

If you're delivering emails in the background, make sure to set Active Job queue adapter to

:test
or
:inline
:
# config/environments/test.rb
Rails.application.configure do |config|
  # ...
  config.active_job.queue_adapter = :test # or :inline
  # ...
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
require "rodauth/migrations"

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

Deadline values

To simplify changes to the database schema, rodauth-rails configures Rodauth to set deadline values for various features in Ruby, instead of relying on the database to set default column values.

You can easily change this back:

set_deadline_values? false

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.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.