DEV Community

Cover image for An Introduction to Auth0 for Ruby on Rails
Thomas Riboulet for AppSignal

Posted on • Originally published at blog.appsignal.com

An Introduction to Auth0 for Ruby on Rails

From custom-made to plug-and-play forms of authentication, Ruby developers have plenty to choose from these days. Yet, as you may know, building your own solution can be costly and dangerous. If Devise is the de facto standard for most teams, an alternative might simplify the lives of most.

This article will cover the setup and use of Auth0 in a Ruby on Rails application, including everything you need to get going properly, from handling roles to relying on multiple providers to authenticate users.

Getting Started

Here's what we need to get started:

  • An Auth0 account
  • A Ruby on Rails application (version 7.x onwards)

Auth0 is a third-party authentication service with a free tier that lets you handle up to 7,000 users. That is plenty to get you started, and its pricing is reasonable if you need more advanced features.

Configuring Our Ruby App in Auth0

Since your application will rely on Auth0 to authenticate users through redirects and calls, we must ensure it stays secure.

In our Auth0 account, let's create an application within a tenant. You can create multiple tenants in an account to separate:

  • Domain names.
  • Different environments (development, production, etc).
  • The country or region within which data will be stored.

Once you have created your first tenant, you can build an application. That's where we'll start.

Head to the "Settings" tab in the application panel. You need to copy and paste the following and save it to a safe place:

  • The domain name: app-name.[eu,us,..].auth0.com
  • The client's ID
  • The client's Secret

We must also fill in the following Application URIs. We'll use these values for our local development setup:

  • Allowed Callback URLs: http://localhost:3000/auth/auth0/callback (the URL Auth0 will redirect to after authentication).
  • Allowed Logout URLs: http://localhost:3000 (the URL Auth0 will redirect to after someone logs out).

Let's go ahead and configure our app.

Preparing Our Ruby on Rails Application

You can start with a vanilla Ruby on Rails application using the rails new command.

Let's generate one that relies on SQLite3 for the database and Tailwind for the CSS library. Skip the installation of mini-test and name the application "Auth0 article":

cd ~/my_projects
rails new -d sqlite3 -c tailwind -T auth0_article
Enter fullscreen mode Exit fullscreen mode

Create a User model with just a few attributes:

cd auth0_article
bin/rails db:create
bin/rails generate model User email name
bin/rails db:migrate
Enter fullscreen mode Exit fullscreen mode

This builds a simple but efficient base for our application.

Adding the Auth0 Gem to the App

You'll need omniauth-auth0 and omniauth-rails_csrf_protection to use Auth0 in your Ruby on Rails application.

gem 'omniauth-auth0', '~> 3.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
Enter fullscreen mode Exit fullscreen mode

Configuring Auth0

We need to create a tiny configuration file (config/auth0.yml) to store credentials in our development environment.

development:
  auth0_domain: <YOUR AUTH0 APPLICATION DOMAIN NAME>
  auth0_client_id: <YOUR AUTH0 APPLICATION CLIENT ID>
  auth0_client_secret: <YOUR AUTH0 APPLICATION CLIENT SECRET>
Enter fullscreen mode Exit fullscreen mode

We can also rely on environment variables here by using some erb:

development:
  auth0_domain: <%= ENV['AUTH0_APPLICATION_DOMAIN'] %>
  auth0_client_id: <%= ENV['AUTH0_APPLICATION_CLIENT_ID'] %>
  auth0_client_secret: <%= ENV['AUTH0_APPLICATION_CLIENT_SECRET'] %>
Enter fullscreen mode Exit fullscreen mode

Of course, relying on Ruby on Rails credentials ensures that things stay more up-to-date.

This file will be used in the Auth0 initializer (config/initializers/auth0.rb), which we will now create:

AUTH0_CONFIG = Rails.application.config_for(:auth0)

Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :auth0,
    AUTH0_CONFIG['auth0_client_id'],
    AUTH0_CONFIG['auth0_client_secret'],
    AUTH0_CONFIG['auth0_domain'],
    callback_path: '/auth/auth0/callback',
    authorize_params: {
      scope: 'openid profile'
    }
  )
end
Enter fullscreen mode Exit fullscreen mode

As you can see here, we are using Rails.application.config_for to load up the content of a YAML file as a convenient hash. We could replace this with any secret handling library, including Rails encrypted credentials storage.

Note the following:

  • The provider name (:auth0)
  • The three items from the YAML file, read and used through the AUTH0_CONFIG hash.
  • The callback_path key and value, matching the one we configured in the Auth0 interface.
  • The authorize_params and the scope key inside it; we will come back to that later.

We need to create the interface and routing elements to allow for authentication.

Setting Up Routes and UI with Tailwind

In this example, we will work with a Ruby on Rails application using version 7.1 of the framework with TailwindCSS.

Use the following command:

rails new -d sqlite3 -c tailwind -T myApp
Enter fullscreen mode Exit fullscreen mode

We can then add two controllers with the index action, preparing to test the authentication process:

bin/rails g controller public index
bin/rails g controller private index
Enter fullscreen mode Exit fullscreen mode

With those two commands, the following are created:

  • Both public_controller.rb and private_controller.rb controllers, with the index action ready to use
  • Both related routes
  • The related views

Let's add the Auth0 controller to handle the callbacks and failures. Create the controller file (bin/rails g controller auth0) and add the following:

# ./app/controllers/auth0_controller.rb
class Auth0Controller < ApplicationController

  # this will happen in case of success
  def callback
    auth_info = request.env['omniauth.auth']
    session[:userinfo] = auth_info['extra']['raw_info']

    redirect_to '/private/index'
  end

  # this will happen in case of failure
  def failure
    @error_msg = request.params['message']
    redirect_to '/public_index'
  end

  # logout route
  def logout
    reset_session
    redirect_to logout_url, allow_other_host: true
  end

  private

  def logout_url
    request_params = {
      returnTo: root_url,
      client_id: AUTH0_CONFIG['auth0_client_id']
    }

    URI::HTTPS.build(host: AUTH0_CONFIG['auth0_domain'], path: '/v2/logout', query: request_params.to_query).to_s
  end
end
Enter fullscreen mode Exit fullscreen mode

Then update the routes to use those three actions:

./config/routes.rb
Rails.application.routes.draw do
  # ..
  get '/auth/auth0/callback' => 'auth0#callback'
  get '/auth/failure' => 'auth0#failure'
  get '/auth/logout' => 'auth0#logout'

  root "public#index"
end
Enter fullscreen mode Exit fullscreen mode

And then we can add Login and Logout buttons in the public and private views, respectively:

# app/views/public/index.html.erb
<div>
  <h1 class="font-bold text-4xl">Public#index</h1>
  <p>Find me in app/views/public/index.html.erb</p>
  <%= button_to 'Login', '/auth/auth0', method: :post, data: { turbo: false }, class: "rounded bg-sky-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-sky-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500" %>
</div>

# app/views/private/index.html.erb
<div>
  <h1 class="font-bold text-4xl">Private#index</h1>
  <p>Find me in app/views/private/index.html.erb</p>
  <%= button_to 'Logout', '/auth0/logout', method: :get, data: { turbo: false }, class: "rounded bg-sky-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-sky-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500" %>
</div>
Enter fullscreen mode Exit fullscreen mode

You can now go to 'http://localhost:3000'. Use the Login button and you'll be redirected to the private page. There, you will find a Logout button.

A couple of things are missing, though:

  • Data from a user's profile
  • Identifying a user and handling authorization

Working with Scopes and Data

Let's take a few steps back and bring back the initializer (config/initializers/auth0.rb):

AUTH0_CONFIG = Rails.application.config_for(:auth0)

Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :auth0,
    AUTH0_CONFIG['auth0_client_id'],
    AUTH0_CONFIG['auth0_client_secret'],
    AUTH0_CONFIG['auth0_domain'],
    callback_path: '/auth/auth0/callback',
    authorize_params: {
      scope: 'openid profile'
    }
  )
end
Enter fullscreen mode Exit fullscreen mode

The critical piece here is the scope: openid profile. This tells Auth0 we are interested in a few pieces of information, namely:

  • The provider name (Auth0)
  • A uid: A unique identifier to match a user
  • An info hash: Containing a name, URL to a profile picture, and an empty 'email' key
  • An extra_info hash: Again, with a name and profile picture, but also a pair of given and family names

Read more on the topic of scopes in Auth0's documentation.

The above information is valuable, and you should ensure that, at least upon first login, you copy that data into a table in your application.

To do so, we need to use the content of the request-response in the auth0_controller and, specifically, in the callback action:

def callback
  # all the data sent by auth0
  auth_info = request.env['omniauth.auth']

  # the data that really identifies the user
  session[:userinfo] = auth_info['extra']['raw_info']

  redirect_to '/private/index'
end
Enter fullscreen mode Exit fullscreen mode

Use a breakpoint before the redirect to dig into the hash and experiment.

We usually want the email address too. To get it, you can update the scope:

scope: 'openid profile email'
Enter fullscreen mode Exit fullscreen mode

After reloading the application server and going through the login steps, you will need to give the application access to additional information.

We can now do something like this in the callback action:

def callback
  # all the data sent by auth0
  auth_info = request.env['omniauth.auth']

  # the data that really identify the user
  session[:userinfo] = auth_info['extra']['raw_info']

  user = User.find_by(email: session[:userinfo]['email'] || User.new
  if user.new_record?
    user.name = session[:userinfo]['name']
    user.email = session[:userinfo]['email']
  end
  user.save

  redirect_to '/private/index'
end
Enter fullscreen mode Exit fullscreen mode

This will ensure we have a local, up-to-date profile for the user.

A Word On Sessions

A session is a special hash in a Ruby on Rails application. It has limited storage that is accessible from the controllers and views and unique to each visitor.

We can "open" and "close" sessions. If we need a specific dataset, we can decide that a session is open. If it has no data, then it's closed.

Remember the following lines in our Auth0 controller:

# in the callback method
session[:userinfo] = auth_info['extra']['raw_info']

# in the logout method
reset_session
Enter fullscreen mode Exit fullscreen mode

The first one writes the value in the raw_info key of the hash to the session hash, while the second one just clears up the session hash.

We can then define the following helper method in the ApplicationHelper:

# get the user
def current_user
  User.find_by(email: session[:userinfo]['email']) if session[:userinfo]
end
Enter fullscreen mode Exit fullscreen mode

A Security Concern

Now let's use this as a concern for our controllers. The idea is to define a controller that requires users to be logged in to access it, such as our private_controller.

We can write the following concern file:

# ./app/controllers/concerns/secured.rb
module Secured
  extend ActiveSupport::Concern

  included do
    before_action :logged_in?
  end

  def logged_in?
    redirect_to '/' unless session[:userinfo].present?
  end
end
Enter fullscreen mode Exit fullscreen mode

We can then use it in our controllers like so:

class PrivateController < ApplicationController
  include Secured

  def index
  end
end
Enter fullscreen mode Exit fullscreen mode

If a visitor isn't logged in, but then tries to open up the /private/index page, they will automatically be redirected to the site's root.

Integrating with Other Providers

You can rely on multiple providers through Auth0. However, Google is defined as the default. For many companies, that's enough.

If you need more providers, head to the Authentication menu for the related tenant in Auth0, and then the Social submenu, where you can set up additional providers.

It's also worth noting that you can configure multi-factor authentication (MFA) with Auth0 too.

The process will remain the same except for those configuration steps. Our application will only return visitor data to prove that a visitor has been authenticated.

Opening Up Towards Authorization

Now we can authenticate users and send their information to our application through a callback. We have already seen how to get a user's email address out of a hash and find the relevant user in our database.

From there, we can define authorization policies.

Pundit is a great choice to define and use authorization policies, yet relies on checking a user's role.

Using a role attribute is, in fact, the easiest method. You can fill it in when creating a user in the Rails console or your application's back-end interface, before a user's first login attempt.

Or you can get a list of admins' emails and match a user's email against the list when creating the user.

ADMINS = ['johndoe@example.com']

def find_or_create_user(email:, name:)
  user = User.find_by(email: session[:userinfo]['email'] || User.new
  if user.new_record?
    user.name = session[:userinfo]['name']
    user.email = session[:userinfo]['email']
  end
  if ADMINS.include?(user.email)
    user.role = 'admin'
  else
    user.role = 'normal'
  end

  user.save
end
Enter fullscreen mode Exit fullscreen mode

And that's it!

What We've Covered

In this article, we set up:

  • A tenant and application in Auth0
  • Auth0-related gems in a Ruby on Rails application
  • Routes and views in the application

We then:

  • Reviewed the concept of a session and saw how to use it
  • Created base tooling to check if a user is logged in
  • Added a controller concern to secure controllers behind a login requirement
  • Saw how to match users to their roles

Auth0 and other authentication providers can help you integrate state-of-the-art authentication in your Ruby on Rails application without writing much code. It's often a much better option than relying on your own implementation of the authentication layer or even using gems like Devise.

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)