DEV Community

Cover image for How to Use the rodauth-omniauth Gem in Ruby
Abiodun Olowode for AppSignal

Posted on • Originally published at blog.appsignal.com

How to Use the rodauth-omniauth Gem in Ruby

Setting up authentication for your Ruby app is important to ensure it is secure. In this post, we'll explore the rodauth-omniauth gem for Ruby to implement authentication in your app via third-party providers.

First, let's quickly define authentication before exploring the benefits of OmniAuth and setting up the rodauth-omniauth gem for a Rails app.

Let's dive straight in!

What Is Authentication?

Authentication is the act of verifying that someone or something is who or what they say they are to grant them access to requested resources. So if user A wants access to an application, they have to provide identification that is acceptable and confirmable by the system to be granted access.

Authentication can take many forms — for example:

  • Providing a password that matches an account's password as stored in an app's database.
  • Using facial recognition or fingerprints.
  • The use of a code sent to a registered channel, like an email or phone number, or an authenticator app.

There are various descriptions of authentication — we have single-factor authentication, two-factor authentication, and multi-factor authentication.

What Does OmniAuth Bring to the Table?

Authentication can be tiring for a user, especially when they have to constantly remember their various usernames and passwords for various applications. One of the solutions OAuth (Open Authentication) provides is to allow one service provider to authenticate a user using their identity with another service provider. This means that as a user, you only need to remember one username and password. Then every other application you need access to can delegate your authentication to your preferred OAuth provider.

Also, you do not need to give your password to the application that requires authentication. Instead, you are redirected to your provider, where you can be authenticated, and then redirected to the application you intend to access (thereby keeping your credentials safe and secure). Your provider, on the other hand, then shares the relevant information about you with the requesting application. Some common OAuth service providers are  Google, Facebook, Twitter, and GitHub.

OmniAuth is a library that standardizes multi-provider authentication for web applications. It uses a strategy pattern for the different providers, and these strategies are generally released individually as Ruby gems. The rodauth-omniauth gem:

Offers login and registration via multiple external providers using OmniAuth, together with the persistence of external identities.

Source: rodauth-omniauth GitHub repo

Setting Up rodauth-omniauth for a Rails Application

Let's start by creating a new Rails project titled "rodauth_test_app".

$ rails new rodauth_test_app
$ cd rodauth_test_app
Enter fullscreen mode Exit fullscreen mode

It is important to note that the rodauth-omniauth gem is an extension to the rodauth gem, and as such, we need to have already set up Rodauth to use it. For a Rails app, a quick way to do that is to run the following commands:

$ bundle add rodauth-rails
$ rails generate rodauth:install
$ rails db:migrate
Enter fullscreen mode Exit fullscreen mode

To have access to the views rodauth provides, you can run either of the following commands:

$ rails g rodauth:views # to have access to all the views that rodauth provides
$ rails g rodauth:views login # to access only the login page which is what we need
Enter fullscreen mode Exit fullscreen mode

The rails rodauth:routes command gives us a list of the routes handled by the RodauthApp, making it possible for us to get to the different pages we're interested in.

Among the tables created during our installation, the most important one to us is the Accounts table. This is the table where all user accounts are stored and will be the reference for the AccountIdentities table we will create.

Creating an Account Identities Table

A person can have a driver's license, international passport, and voter's card. All these means of identification point to the same person and can be used interchangeably.

Similarly, a user can have several means of identification (account identities), and this user may choose to log in at any time using any of these identities. That is why we'll create an AccountIdentities table to store all the identities of a specific user account, identified by a unique email address.

With a quick look in our db folder at the ..._create_rodauth.rb file, we find the schema for the accounts table as follows:

create_table :accounts do |t|
  t.integer :status, null: false, default: 1
  t.string :email, null: false
  t.index :email, unique: true, where: "status IN (1, 2)"
  t.string :password_hash
end
Enter fullscreen mode Exit fullscreen mode

To create a table to store our account identities, we run the following command:

$ rails g migration CreateAccountIdentities
Enter fullscreen mode Exit fullscreen mode

We find the generated file in the db/migrate folder. Let's go ahead and define its schema.

class CreateAccountIdentities < ActiveRecord::Migration[7.0]
  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.index [:provider, :uid], unique: true
      # timestamps are not included -> to be explained later
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Within this table, we ensure the following:

  • Every identity must belong to an account.
  • When an account is deleted, all associated identities also get deleted.
  • The provider must be declared (e.g., Google, Twitter, Facebook, GitHub).
  • A uid must be present.
  • The combination of the provider and the uid must be unique to ensure that all account identities are unique for each provider.

It is also very important to have a homepage — a root route — to redirect users to after they are successfully authenticated. To ensure that, create a home controller with an index method that serves as the homepage.

$ rails g controller home index
Enter fullscreen mode Exit fullscreen mode

In our config/routes.rb file, we add the root route as follows:

root "home#index"
Enter fullscreen mode Exit fullscreen mode

Setting the OAuth Provider

We'll use GitHub as the OAuth provider for this app, so install the GitHub OAuth gem and the rodauth-omniauth gem.

$ bundle add rodauth-omniauth omniauth-github
Enter fullscreen mode Exit fullscreen mode

Having installed these gems, enable the omniauth feature and register your OAuth provider in the Rodauth configuration. The config file can be found at app/misc/rodauth_main.rb.

configure do
# ...
  enable :omniauth
  omniauth_provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], name: :github
  # name is only important here if it's different from the provider e.g. :google_oauth2 with name: :google
# ...
end
Enter fullscreen mode Exit fullscreen mode

To get the client id and client secret for this app, we need to create the OAuth app and set the callback URL to {root_url}/auth/{provider}/callback. In our case, this translates to https://localhost:3000/auth/github/callback.

At this point, we're all set to start our server and visit our homepage.

$ rails s
Enter fullscreen mode Exit fullscreen mode

Visiting /login, as confirmed from our rodauth routes, leads to the login page. It is on this page that we add our Login via GitHub button.

# app/views/rodauth/_login_form_footer.html.erb
# ...
  <li><%= button_to "Login via GitHub", rodauth.omniauth_request_path(:github), method: :post, data: { turbo: false } %></li>
# ...
Enter fullscreen mode Exit fullscreen mode

Authentication and Redirection with Rodauth

On clicking the Login via GitHub button, we are redirected to the GitHub authentication page, assuming that the providers are properly configured. On this page, we are required to provide some details for authentication and after a successful authentication, we're redirected to the root URL of our application.

Between authentication and redirection to the homepage, Rodauth does the following:

  • Creates an account, along with an account identity, if no account with that email previously existed.
  • Assigns an identity to an account if an account with that email already exists.
  • Sets the status of a newly created account to "verified", because it assumes that the external provider verified the email address.
  • Returns an error response during the callback phase if an account associated with the external identity already exists and is unverified (e.g., it is created through normal registration), as only verified accounts can be logged into.

Accessing Account Identities

With rodauth-omniauth version 0.3.3 and above, you can access the identities related to an account without any further setup, as shown below:

> Account.first
=> # <Account:0x00007f236b455a70 id: 1, status: "verified", email: "biodun9@gmail.com", password_hash: nil>

> Account.first.identities
=> [#<Account::Identity:0x00007fe90a0612d8
  id: 1,
  account_id: 2,
  provider: "github",
  uid: "52589264"]
Enter fullscreen mode Exit fullscreen mode

This is possible because during the installation of rodauth-rails, rodauth-model is automatically configured within the account model, defining an identities one-to-many association on the account model.

class Account < ApplicationRecord
  include Rodauth::Rails.model # the rodauth-model is included here
  enum :status, unverified: 1, verified: 2, closed: 3
end

Enter fullscreen mode Exit fullscreen mode

For versions below 0.3.3, this isn't the case due to a bug. So to access identities, you can either choose to upgrade your version of rodauth-omniauth, or define the relationship manually like this:

# ... app/models/account_identity.rb
class AccountIdentity < ApplicationRecord
  belongs_to :account
end

# ... app/models/account.rb
class Account < ApplicationRecord
  has_many :identities, class_name: "AccountIdentity"
end
# ...
Enter fullscreen mode Exit fullscreen mode

Adding Extra Columns

During the identities table migration, the timestamps column was missing. This is because that column is not originally included in the implementation provided by the gem.

However, we can manually add that column (as well as any other columns) and any logic required to configure the gem.

$ rails g migration add_timestamps_to_account_identities
Enter fullscreen mode Exit fullscreen mode
class AddTimestampsToAccountIdentities < ActiveRecord::Migration[7.0]
  def change
    add_timestamps :account_identities, null: true
    # allowing this field to be nullable due to already existing records
  end
end
Enter fullscreen mode Exit fullscreen mode

Within the configuration, we have to implement the logic to add these columns when creating an account identity.

# ... app/misc/rodauth_main.rb
configure do
# ...
  # omniauth_identity_insert_hash handles account identity creation
  omniauth_identity_insert_hash do
    super().merge(created_at: Time.now)
  end

  # omniauth_identity_update_hash handles account identity updates
  omniauth_identity_update_hash do
    super().merge(updated_at: Time.now)
  end
# ...
end
Enter fullscreen mode Exit fullscreen mode

On deleting the previous account identity and creating a new one, we get this:

> AccountIdentity.first
=> #<AccountIdentity:0x00007f28533b83c8
 id: 1,
 account_id: 1,
 provider: "github",
 uid: "52589264",
 created_at: Sat, 18 Feb 2023 09:04:10.069443000 UTC +00:00,
 updated_at: Sat, 18 Feb 2023 09:04:10.069430000 UTC +00:00>
Enter fullscreen mode Exit fullscreen mode

If it becomes necessary to access the auth hash or any other additional information, this gem provides us with some helper methods, like omniauth_provider, omniauth_email, and many more. Hooks are also available to implement the logic required at certain phases like omniauth_before_callback_phase, omniauth_on_failure, etc. A comprehensive list of these helper methods and hooks can be found in the rodauth-omniauth documentation.

A Note on Compatibility and Versioning

When installing the omniauth-google_oauth2 gem, a versioning error is encountered.

Bundler could not find compatible versions for gem "omniauth":
  In Gemfile:
    omniauth-google_oauth2 was resolved to 0.1.5, which depends on
      omniauth (~> 1.0)

    rodauth-omniauth was resolved to 0.3.1, which depends on
      omniauth (~> 2.0)
Enter fullscreen mode Exit fullscreen mode

This is because the omniauth version dependencies for both gems are not compatible. omniauth-google_oauth2 depends on omniauth ~> 1.0, while the rodauth-omniauth gem depends on omniauth ~> 2.0. A more compatible omniauth-google gem would be omniauth-google-oauth2.

Any OAuth provider gem used alongside rodauth-omniauth must be compatible with OmniAuth 2.0.

rodauth-omniauth: Now and Future Plans

In summary, the rodauth-omniauth gem, though powerful, cannot be employed independently as it is a Rodauth extension. However, when employed with Rodauth, it saves you time that would have been spent manually implementing OmniAuth logic.

The gem handles:

  • Routing a request to a third-party provider.
  • The response (by creating the required accounts/identities or validating existing ones).
  • Outputting errors when necessary.

If the login is successful, it also redirects the request to the root URL.

In the future, the plan is for the rodauth-omniauth gem to offer services like allowing users to connect or remove external identities when signed in. So you will have the option to connect your Google account to an application (even if you're signed in with GitHub) or disconnect an already existing identity from an application while signed in.

Wrapping Up

In this post, we looked briefly at the usefulness of OmniAuth before setting up rodauth-omniauth for a Rails application. We also defined authentication and explained why it is so important for your Ruby app.

I hope you've found this introduction to the rodauth-omniauth gem useful.

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Top comments (0)