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
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
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
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
To create a table to store our account identities, we run the following command:
$ rails g migration CreateAccountIdentities
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
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
In our config/routes.rb
file, we add the root route as follows:
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.
$ 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
.
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 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
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>
# ...
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"]
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
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
# ...
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
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.
# ... 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:
> 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.
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)
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)