How to Use the rodauth-omniauth Gem in Ruby
Tags: ruby
Published: 2023-04-12 10:52:58

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](https://oauth.net/) (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](https://github.com/omniauth/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](https://github.com/janko/rodauth-omniauth)

## Setting Up rodauth-omniauth for a Rails Application

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

```sh
$ rails new rodauth_test_app
$ cd rodauth_test_app
```

It is important to note that the [rodauth-omniauth](https://github.com/janko/rodauth-omniauth) gem is an extension to the [rodauth](https://github.com/jeremyevans/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:

```sh
$ bundle add rodauth-rails
$ rails generate rodauth:install
$ rails db:migrate
```

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

```sh
$ 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
```

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:

```ruby
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
```

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

```sh
$ rails g migration CreateAccountIdentities
```

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

```ruby
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
```

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.

```sh
$ rails g controller home index
```

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

```ruby
root "home#index"
```

### 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.

```sh
$ bundle add rodauth-omniauth omniauth-github
```

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`.

```ruby
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
```

To get the client id and client secret for this app, we need to [create the OAuth app](https://github.com/settings/developers) 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.

```sh
$ rails s
```

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.

```ruby
# 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>
# ...
```

### 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:

```ruby
> 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"]
```

This is possible because during the installation of `rodauth-rails`, [rodauth-model](https://github.com/janko/rodauth-model) is automatically configured within the `account` model, defining an `identities` one-to-many association on the account model.

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

```

For versions below `0.3.3`, this isn't the case [due to a bug](https://github.com/janko/rodauth-omniauth/issues/10). So to access `identities`, you can either choose to upgrade your version of `rodauth-omniauth`, or define the relationship manually like this:

```ruby
# ... 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
# ...
```

### 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.

```sh
$ rails g migration add_timestamps_to_account_identities
```

```ruby
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
```

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

```ruby
# ... 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
```

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

```ruby
> 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>
```

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](https://github.com/janko/rodauth-omniauth).

## A Note on Compatibility and Versioning

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

```sh
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)
```

This is because the `omniauth` version dependencies for both gems are not compatible. [`omniauth-google_oauth2`](https://rubygems.org/gems/omniauth-google_oauth2) depends on `omniauth ~> 1.0`, while the [`rodauth-omniauth`](https://rubygems.org/gems/rodauth-omniauth) gem depends on `omniauth ~> 2.0`. A more compatible `omniauth-google` gem would be [`omniauth-google-oauth2`](https://rubygems.org/gems/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](https://blog.appsignal.com/ruby-magic)!**