ruby

An Introduction to Auth0 for Ruby on Rails

Thomas Riboulet

Thomas Riboulet on

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

Shell
cd ~/my_projects rails new -d sqlite3 -c tailwind -T auth0_article

Create a User model with just a few attributes:

Shell
cd auth0_article bin/rails db:create bin/rails generate model User email name bin/rails db:migrate

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.

Ruby
gem 'omniauth-auth0', '~> 3.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'

Configuring Auth0

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

YAML
development: auth0_domain: <YOUR AUTH0 APPLICATION DOMAIN NAME> auth0_client_id: <YOUR AUTH0 APPLICATION CLIENT ID> auth0_client_secret: <YOUR AUTH0 APPLICATION CLIENT SECRET>

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

YAML
development: auth0_domain: <%= ENV['AUTH0_APPLICATION_DOMAIN'] %> auth0_client_id: <%= ENV['AUTH0_APPLICATION_CLIENT_ID'] %> auth0_client_secret: <%= ENV['AUTH0_APPLICATION_CLIENT_SECRET'] %>

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:

Ruby
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

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:

Shell
rails new -d sqlite3 -c tailwind -T myApp

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

Shell
bin/rails g controller public index bin/rails g controller private index

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:

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

Then update the routes to use those three actions:

Ruby
./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

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

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

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

Ruby
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

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:

Ruby
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

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:

Ruby
scope: 'openid profile email'

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:

Ruby
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

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:

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

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:

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

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:

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

We can then use it in our controllers like so:

Ruby
class PrivateController < ApplicationController include Secured def index end end

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.

Ruby
ADMINS = ['[email protected]'] 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

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!

Thomas Riboulet

Thomas Riboulet

Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.

All articles by Thomas Riboulet

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps