logo <

Multifactor Authentication in Rails with Rodauth

Multi-factor authentication or MFA (generalized two-factor authentication or 2FA) is a method of authentication where the user is required to provide two or more pieces of evidence (“factors”) in order to be granted access. Typically the user would first prove knowledge of something only they know (e.g. their password), and then prove posession of something only they own (e.g. another device). This provides an extra layer of security for the user’s account.

Most common multifactor authentication methods include:

  • TOTP (Time-based One-Time Passwords) – user has an app installed on their device that displays the authentication code, which is refreshed every 30 seconds

  • SMS codes – user receives authentication codes on their phone via SMS when the application requests them

  • Recovery codes – user is given a fixed set of one-time codes they can enter when logging in (this is typically used as a backup method)

  • WebAuthn – user authenticates themselves using a security key or built-in platform biometric sensors (e.g. fingerprint)

In this article, I want to show you how to add multifactor authentication to a Rails app using Rodauth, which has built-in support for each of the multifactor authentication methods mentioned above. Compared to alternatives1, Rodauth provides a much more integrated experience by shipping with complete endpoints, default HTML templates, session management, lockout logic and more2. To keep the tutorial focused, we’ll be implementing just the first three methods, as they’re by far the most common.

We’ll be using the rodauth-rails gem, and we’ll be continuing off of the application we started building in my previous article. The goal functionality: allow users to set up TOTP as their primary MFA method, and use SMS codes and recovery codes as backup MFA methods.

TOTP

The TOTP functionality is provided by Rodauth’s otp feature. It depends on the rotp and rqrcode gems, so let’s first install those:

$ bundle add rotp rqrcode

Next, we need to create the required database table. For this we’ll use the migration generator provided by rodauth-rails:

$ rails generate rodauth:migration otp
# create  db/migrate/20201214200106_create_rodauth_otp.rb

$ rails db:migrate
# == 20201214200106 CreateRodauthOtp: migrating =======================
# -- create_table(:account_otp_keys)
# == 20201214200106 CreateRodauthOtp: migrated ========================

Now we can enable the otp feature in our Rodauth configuration:

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    enable :otp
  end
end

This adds the following routes to our application:

  • /otp-auth – authenticate via TOTP code
  • /otp-setup – set up TOTP authentication
  • /otp-disable – disable TOTP authentication
  • /multifactor-manage – set up or disable available MFA methods
  • /multifactor-auth – authenticate via available MFA methods
  • /multifactor-disable – disable all MFA methods

To allow the user to configure MFA, let’s display a link to the /multifactor-manage route for managing MFA methods in our views:

<!-- app/views/application/_navbar.html.erb -->
<% if rodauth.logged_in? %>
  <!-- ... --->
  <%= link_to "Manage MFA", rodauth.two_factor_manage_path, class: "dropdown-item" %>
  <!-- ... --->
<% end %>

Now when the user logs in and clicks on “Manage MFA”, they’ll get redirected to the OTP setup page that Rodauth provides out-of-the-box3:

Rodauth OTP setup page

The user can now scan the QR code using an authenticator app such as Google Authenticator, Microsoft Authenticator or Authy, and enter the OTP code (along with their current password) to finish setting up OTP. As a developer, you can generate the code from the OTP secret using the ROTP gem:

$ rotp --secret omo2p3movepqyc222rp54v3cic7ky2au
409761

When the user with OTP set up logs in the next time, we want them to be automatically redirected to the OTP auth page. We can achieve this by requiring logged in users that have MFA set up to authenticate with 2nd factor, and tweaking the flash messages to make it feel like part of one signin:

# app/misc/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
  # ...
  route do |r|
    # ...
    # require MFA if the user is logged in and has MFA setup
    if rodauth.uses_two_factor_authentication?
      rodauth.require_two_factor_authenticated
    end
  end
end
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    # don't show error message when redirected after login
    two_factor_need_authentication_error_flash { flash[:notice] == login_notice_flash ? nil : super() }
    # show generic authentication message
    two_factor_auth_notice_flash { login_notice_flash }
  end
end

Rodauth TOTP authentication page

Recovery codes

After the user sets up TOTP, it’s recommended to also generate a set of “recovery” codes for them to save somewhere, which they can use on login in case they lose access to their TOTP device. This functionality is provided by Rodauth’s recovery_codes feature.

Let’s start by creating the required database table:

$ rails generate rodauth:migration recovery_codes
# create  db/migrate/20201214200106_create_rodauth_recovery_codes.rb

$ rails db:migrate
# == 20201217071036 CreateRodauthRecoveryCodes: migrating =======================
# -- create_table(:account_recovery_codes, {:primary_key=>[:id, :code]})
# == 20201217071036 CreateRodauthRecoveryCodes: migrated ========================

And enabling the recovery_codes feature in our Rodauth configuration:

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    enable :otp, :recovery_codes
  end
end

This adds the following routes to our app:

  • /recovery-auth – authenticate via a recovery code
  • /recovery-codes – view & add recovery codes

We’ll now override the after_otp_setup hook to display recovery codes to the user after they’ve successfully set up TOTP, instead of the default redirect.

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    # automatically generate recovery codes after enabling first MFA method
    auto_add_recovery_codes? true
    # automatically remove recovery codes after disabling last MFA method
    auto_remove_recovery_codes? true
    # display recovery codes after TOTP setup
    after_otp_setup do
      set_notice_now_flash "#{otp_setup_notice_flash}, please make note of your recovery codes"
      return_response add_recovery_codes_view
    end
  end
end

We’ll also override the default Rodauth template to display the recovery codes in a nicer way. For convenience, we’ll add a download link for the recovery codes as well. Instead of adding a new endpoint, which would have to be password-protected to maintain security, we’ll just implement the download link in plain HTML using a data URL and download attribute.

$ rails generate rodauth:views recovery_codes
<!-- app/views/rodauth/add_recovery_codes.html.erb -->
<% content_for :title, rodauth.add_recovery_codes_page_title %>

<% if rodauth.recovery_codes.any? %>
  <p class="my-3">
    Copy these recovery codes to a safe location.
    You can also download them <%= link_to "here", "data:,#{rodauth.recovery_codes.join("\n")}", download: "myapp-recovery-codes.txt" %>.
  </p>

  <div class="d-inline-block mb-3 border border-info rounded px-3 py-2">
    <% rodauth.recovery_codes.each_slice(2) do |code1, code2| %>
      <div class="row text-info text-left">
        <div class="col-lg my-1 font-monospace"><%= code1 %></div>
        <div class="col-lg my-1 font-monospace"><%= code2 %></div>
      </div>
    <% end %>
  </div>
<% end %>

<!-- Used for filling in missing recovery codes later on -->
<% if rodauth.can_add_recovery_codes? %>
  <%== rodauth.add_recovery_codes_heading %>
  <%= render template: "rodauth/recovery_codes", layout: false %>
<% end %>

When the user now sets up TOTP, they will be shown a page like this:

Rodauth page for viewing and downloading recovery codes

And when they log into their account the next time, on the multifactor auth page they can choose to enter a recovery code instead of TOTP.

Multifactor auth page with OTP and recovery codes options

SMS codes

In addition to TOTP, it’s good practice to also provide the ability to use SMS codes for 2nd factor authentication. Rodauth provides a specialized sms_codes feature for this.

To set it up, we again create the required database table:

$ rails generate rodauth:migration sms_codes
# create  db/migrate/20201219173710_create_rodauth_sms_codes.rb

$ rails db:migrate
# == 20201219173710 CreateRodauthSmsCodes: migrating ==================
# -- create_table(:account_sms_codes)
# == 20201219173710 CreateRodauthSmsCodes: migrated ===================

And enable the sms_codes feature in the Rodauth configuration:

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    enable :otp, :recovery_codes, :sms_codes
  end
end

This adds the following routes to our app:

  • /sms-request – request the SMS code to be sent
  • /sms-auth – authenticate via an SMS code
  • /sms-setup – set up SMS codes authentication
  • /sms-confirm – confirm the provided phone number
  • /sms-disable – disable SMS codes authentication

When an SMS code is requested, Rodauth calls the sms_send method with the configured phone number and a corresponding text message. This method isn’t defined by default, since Rodauth doesn’t know how we want to send the SMS, instead we’re expected to implement sms_send:

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    sms_send do |phone, message|
      # we need to implement this
    end
  end
end

We’ll use Twilio for sending SMS messages. Assuming we’ve set up an account, we’ll add the account SID, auth token, and phone number to Rails credentials:

$ rails credentials:edit
twilio:
  account_sid: <YOUR_ACCOUNT_SID>
  auth_token: <YOUR_AUTH_TOKEN>
  phone_number: <YOUR_PHONE_NUMBER>

Next, we’ll install the twilio-ruby gem, and create a wrapper class for the Twilio client that uses the configured credentials:

$ bundle add twilio-ruby
# app/misc/twilio_client.rb
class TwilioClient
  Error              = Class.new(StandardError)
  InvalidPhoneNumber = Class.new(Error)

  def initialize
    @account_sid = Rails.application.credentials.twilio.account_sid!
    @auth_token = Rails.application.credentials.twilio.auth_token!
    @phone_number = Rails.application.credentials.twilio.phone_number!
  end

  def send_sms(to, message)
    client.messages.create(from: @phone_number, to: to, body: message)
  rescue Twilio::REST::RestError => error
    # more details here: https://www.twilio.com/docs/api/errors/21211
    raise TwilioClient::InvalidPhoneNumber, error.message if error.code == 21211
    raise TwilioClient::Error, error.message
  end

  def client
    Twilio::REST::Client.new(@account_sid, @auth_token)
  end
end

Finally, we’ll implement sms_send using our new TwilioClient class. We’ll convert SMS sending errors into validation errors, making sure we roll back the wrapping database transaction to prevent phone number & code from being persisted:

# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
  configure do
    # ...
    sms_send do |phone, message|
      twilio = TwilioClient.new
      twilio.send_sms(phone, message)
    rescue TwilioClient::Error => error
      db.rollback_on_exit
      throw_error_status(422, sms_phone_param, sms_invalid_phone_message) if error.is_a?(TwilioClient::InvalidPhoneNumber)
      throw_error_status(500, sms_phone_param, "sending the SMS code failed")
    end
  end
end

When the user now visits the SMS authentication setup page on the multifactor manage page, they can enter their phone number and password, and then enter the SMS code they received to finish the SMS authentication setup.

Rodauth SMS authentication setup page

Afterwards, when the user logs in the next time, in addition to authenticating via TOTP or a recovery code, they’ll now also be able to choose to authenticate via SMS.

Disabling multifactor authentication

In addition to setup and authentication, Rodauth also provides endpoints for disabling any MFA method, which require the user to confirm their password:

  • /otp-disable – disable OTP authentication
  • /sms-disable – disable multifactor authentication
  • /multifactor-disable – disable all multifactor methods

The links for disabling MFA methods that have previously been set up are automatically displayed on the multifactor manage page:

Rodauth links for disabling configured MFA methods

Disabling a MFA method will take care of deleting any records associated to that account from the corresponding database table.

Closing words

In this tutorial we’ve shown how to add multifactor authentication functionality in Rails with Rodauth and rodauth-rails. We’ve enabled the user to set up TOTP as their primary MFA method, after which they receive a set of recovery codes, and have the possibility to also set up SMS as a backup MFA method.

We’ve seen that Rodauth ships with complete endpoints and default HTML templates for managing multiple MFA methods, and generally provides a much more integrated experience compared to the alternatives. Given that multifactor authentication is becoming an increasingly common requirement, it’s very useful to have a framework that supports it with the same level of standard as the other authentication features.

  1. At the time of writing, most popular alternatives are devise-two-factor, active_model_otp, and two_factor_authentication

  2. See the source code for OTP, SMS Codes, Recovery Codes, and Two Factor Base for more details. 

  3. You can override the default template by running rails generate rodauth:views otp and modifying app/views/rodauth/otp_setup.html.erb

Janko Marohnić