rails logo
ruby logo

Rails Authentication By Example

v7
early release
Published on January 20, 2023
You are reading the Early Release version of this guide!
This is our initial take on what we believe is an idiomatic implementation of authentication in Rails.
As you read this guide, please help us shape its final release with your feedback: what should we start, continue, or stop doing?

The Rails examples in this guide help you learn the following security concepts:

  • How to add user login, sign-up, and logout to Rails web applications.
  • How to create route guards to protect Rails web application routes.
  • How to get user profile information to personalize a Rails user interface.
  • How to make API calls from a Rails web app to request data from an external protected API.

This guide uses the Ruby on Rails Auth0 SDK, which provides developers with a high-level API to handle many authentication implementation details. You can secure your Rails applications following security best practices while writing less code.

Rails Code Sample Specs

This code sample uses the following tools:

  • Ruby v3.1.2
  • Rails v7.0.3
  • Ruby on Rails Auth0 SDK v3.0

The Ruby on Rails project dependency installations were tested with bundler v2.3.19. Running the Ruby on Rails web application was tested using Ruby v3.1.2.

Quick Rails Setup

With the help of Auth0, you don't need to be an expert on identity protocols, such as OAuth 2.0 or OpenID Connect, to understand how to secure your web application stack.

You first integrate your Rails web application with Auth0. Your application will then redirect users to an Auth0 customizable login page when they need to log in. Once your users log in successfully, Auth0 redirects them back to your Rails web app, returning JSON Web Tokens (JWTs) with their authentication and user information.

Get the Rails Starter Web Application

We have created a starter project using the Rails CLI to help you learn Rails security concepts through hands-on practice.

Start by cloning the web-app_rails_ruby_hello-world repository on its starter branch:

COMMAND
git clone https://github.com/auth0-developer-hub/web-app_rails_ruby_hello-world.git -b starter --single-branch

Once you clone the repo, make web-app_rails_ruby_hello-world your current directory:

COMMAND
cd web-app_rails_ruby_hello-world

Install the Rails project dependencies as follows:

COMMAND
bundle install

A Rails web application project needs an input secret for the application's key generator. Rails uses this secret key to create all ActiveSupport::MessageVerifier and ActiveSupport::MessageEncryptor instances, including the ones that sign and encrypt cookies.

Run the following command to generate an appropriate value for that secret key:

COMMAND
bin/rails secret

Copy the long-string output from the terminal and paste it into the following input box:

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes it easy to copy and paste code as you follow along.

In this Rails project, you'll define any environment variables in a .env file, which you can then integrate into your Rails project using the dotenv gem.

As such, create a .env file under the root project directory:

COMMAND
touch .env

Then, populate the .env file with the following environment variables:

.env
PORT=4040
SECRET_KEY_BASE=KEY-VALUE
  • The PORT variable represents the server port from where your application will run in development mode.
  • The SECRET_KEY_BASE is the input secret for the application's key generator you created earlier.

Rails automatically loads the PORT and SECRET_KEY_BASE environment variables into your project. You don't need to make any changes to the project code to use them.

Finally, open another terminal tab and execute this command to run your Rails web application in development mode:

COMMAND
bin/rails s

Navigate to http://localhost:4040 to see the home page of your Rails web app.

You are ready to start implementing user authentication in this Rails project. First, you'll need to configure the Rails web application to connect successfully to Auth0. Afterward, you'll use the Ruby on Rails Auth0 SDK to log in users, protect routes, display user profile information, and request protected data from an external API server to hydrate some of the application pages.

Configure Rails with Auth0

Follow these steps to get started with the Auth0 Identity Platform quickly:

Sign up and create an Auth0 Application

A free account also offers you:

During the sign-up process, you create something called an Auth0 Tenant, representing the product or service to which you are adding authentication.

Once you sign in, Auth0 takes you to the Dashboard. In the left sidebar menu, click on "Applications".

Then, click the "Create Application" button. A modal opens up with a form to provide a name for the application and choose its type. Use the following values:

Name
Auth0 Rails Web App Code Sample
Application Type
Regular Web Applications
Regular Web Applications

Click the "Create" button to complete the process. Your Auth0 application page loads up.

In the next step, you'll learn how to help Rails and Auth0 communicate.

What's the relationship between Auth0 Tenants and Auth0 Applications?

Let's say that you have a photo-sharing Rails app called "Rubygram". You then would create an Auth0 tenant called rubygram. From a customer perspective, Rubygram is that customer's product or service.

Now, say that Rubygram is available on three platforms: web as a single-page application and Android and iOS as a native mobile application. If each platform needs authentication, you need to create three Auth0 applications to provide the product with everything it needs to authenticate users through that platform.

Rubygram users belong to the Auth0 Rubygram tenant, which shares them across its Auth0 applications.

Create a communication bridge between Rails and Auth0

When using the Auth0 Identity Platform, you don't have to build login forms. Auth0 offers a Universal Login Page to reduce the overhead of adding and managing authentication.

How does Universal Login work?

Your Rails web application will redirect users to Auth0 whenever they trigger an authentication request. Auth0 will present them with a login page. Once they log in, Auth0 will redirect them back to your Rails web application. For that redirecting to happen securely, you must specify in your Auth0 Application Settings the URLs to which Auth0 can redirect users once it authenticates them.

As such, click on the "Settings" tab of your Auth0 Application page, locate the "Application URIs" section, and fill in the following values:

Allowed Callback URLs
http://localhost:4040/auth/auth0/callback

The above value is the URL that Auth0 can use to redirect your users after they successfully log in.

Allowed Logout URLs
http://localhost:4040

The above value is the URL that Auth0 can use to redirect your users after they log out.

Allowed Web Origins
http://localhost:4040

Using the Ruby on Rails Auth0 SDK, your Rails web application will make requests under the hood to an Auth0 URL to handle authentication requests. As such, you need to add your Rails web application origin URL to avoid Cross-Origin Resource Sharing (CORS) issues.

Scroll down and click the "Save Changes" button.

Do not close this page yet. You'll need some of its information in the next section.

Add the Auth0 configuration variables to Rails

From the Auth0 Application Settings page, you need the Auth0 Domain, Client ID and Client Secret values to allow your Rails web application to use the communication bridge you created.

What exactly is an Auth0 Domain, an Auth0 Client ID, and Auth0 Client Secret?

Domain When you created a new Auth0 account, Auth0 asked you to pick a name for your Tenant. This name, appended with auth0.com, is your Auth0 Domain. It's the base URL that you will use to access the Auth0 APIs and the URL where you'll redirect users to log in.

You can also use custom domains to allow Auth0 to do the authentication heavy lifting for you without compromising your branding experience.

Client ID

Each application is assigned a Client ID upon creation, which is an alphanumeric string, and it's the unique identifier for your application (such as q8fij2iug0CmgPLfTfG1tZGdTQyGaTUA). You cannot modify the Client ID. You will use the Client ID to identify the Auth0 Application to which the Ruby on Rails Auth0 SDK needs to connect.

Client Secret

This secret protects your resources by only granting tokens to requestors if they're authorized. Think of it as your application's password, which must be kept confidential at all times. If anyone gains access to your Client Secret, they can impersonate your application and access protected resources.

Head back to your Auth0 application page and click on the "Settings" tab.

Locate the "Basic Information" section and follow these steps to get the Auth0 Domain, Auth0 Client ID, and Auth0 Client Secret values:

Auth0 application settings to enable user authentication

These variables let your Rails web application identify itself as an authorized party to interact with the Auth0 authentication server.

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes it easy to copy and paste code as you follow along.

As such, enter the "Domain" and "Client ID" values in the following fields to set up your single-page application in the next section:

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page! As an extra precaution, you should use values from an Auth0 test application instead of a production one.

Now, update the .env file under the Rails project directory as follows:

.env
PORT=4040
SECRET_KEY_BASE=KEY-VALUE
AUTH0_CALLBACK_PATH=/auth/auth0/callback
AUTH0_DOMAIN=AUTH0-DOMAIN
AUTH0_CLIENT_ID=AUTH0-CLIENT-ID
AUTH0_CLIENT_SECRET=AUTH0-CLIENT-SECRET

You'll learn more about the role of the AUTH0_CALLBACK_PATH environment variable later on. For now, you need to get the value for the AUTH0_CLIENT_SECRET.

Head back to the "Settings" tab of your Auth0 application page in the Auth0 Dashboard to get the value for AUTH0_CLIENT_SECRET.

Locate the "Client Secret" field, copy its value, and paste it as the AUTH0_CLIENT_SECRET environment value in the .env file.

This page does not store the value of the Auth0 Client Secret. Not even in memory. Think of it as your application's password, which must be kept confidential at all times. If anyone gains access to your Client Secret, they can impersonate your application and access protected resources. The Auth0 Client Secret protects your resources by only granting authentication-related credentials in the form of tokens to requestors if they're authorized.

You've completed the process of adding the Auth0 configuration variables to Rails.

Set Up the Ruby on Rails Auth0 SDK

The Ruby on Rails Auth0 SDK, omniauth-auth0 gem, provides you with an OmniAuth strategy to power up Rails web authentication with Auth0.

As such, update your Gemfile with the following content:

Gemfile
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '~> 3.1'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem 'rails', '~> 7.0.3', '>= 7.0.3.1'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem 'sprockets-rails'
# Use the Puma web server [https://github.com/puma/puma]
gem 'puma', '~> 5.0'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem 'importmap-rails'
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem 'stimulus-rails'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem 'jbuilder'
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
# An OmniAuth strategy to log in with Auth0.
gem 'omniauth-auth0', '~> 3.0'
# Prevents forged authentication requests
gem 'omniauth-rails_csrf_protection', '~> 1.0'
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem 'debug', platforms: %i[mri mingw x64_mingw]
# ENV variables management https://github.com/bkeepers/dotenv
gem 'dotenv-rails'
gem 'pry'
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem 'web-console'
end

Run the following command to install these new dependencies:

COMMAND
bundle install

Create an Auth0 configuration file

Create an auth0.yml file under the config directory to make Rails aware of the environment variables present in your .env file:

COMMAND
touch config/auth0.yml

Populate the config/auth0.yml file with the following content:

config/auth0.yml
development:
auth0_domain: <%= ENV['AUTH0_DOMAIN'] %>
auth0_client_id: <%= ENV['AUTH0_CLIENT_ID'] %>
auth0_client_secret: <%= ENV['AUTH0_CLIENT_SECRET'] %>
auth0_callback_path: <%= ENV['AUTH0_CALLBACK_PATH'] %>

Now, you need to define an Auth0 initializer file in Rails that configures the OmniAuth middleware from the omniauth-auth0 gem with your Auth0 values.

Create an auth0.rb file under the config/initializers directory:

COMMAND
touch config/initializers/auth0.rb
Rails sorts and loads the files in config/initializers and its subdirectories one by one as part of the load_config_initializers initializer. Learn more details about Rails initializer files by visiting the "Using Initializer Files" Rails document.

Next, populate the content of the config/initializers/auth0.rb file with the following code:

config/initializers/auth0.rb
# frozen_string_literal: true
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: AUTH0_CONFIG['auth0_callback_path'],
authorize_params: {
scope: 'openid profile email'
}
)
end
OmniAuth.config.on_failure = Proc.new { |env|
OmniAuth::FailureEndpoint.new(env).redirect_to_failure
}

Let's break down the initializer code.

First, the auth0.rb initializer file uses Rails::Application.config_for to load the config/auth0.yml configuration file data into the AUTH0_CONFIG variable.

The OmniAuth library standardizes multi-provider authentication for web applications, which means that you can configure multiple authentication strategies using that library. For this guide, you'll configure an Auth0 strategy that allows you to authenticate users in your Rails web application using the Auth0 Authentication API.

The built-in OmniAuth::Builder class gives you an easy way to specify multiple strategies using Rails middleware as you've done in the code above. The omniauth-auth0 gem, which is based on Omniauth, encapsulates the OmniAuth strategy that lets your users log in to your Rails web app using Auth0.

OmniAuth provides you with the POST /auth/:provider endpoint, where :provider is the name of the strategy, in this case, auth0. Your Rails web application uses that endpoint to trigger the authentication process with the Auth0 Authentication Server. It passes all the parameters that your Rails web application needs to identify itself as an authorized party to interact with the Auth0 authentication server.

Continuing with the initializer code exploration, let's take a look at the parameters of the provider() OmniAuth method. The first parameter is the :auth0 symbol, OmniAuth takes the name of the class inheriting from OmniAuth::Strategies and transforms it into a Ruby symbol. In the case of the omniauth-auth0 strategy, the class is called Auth0 so the provider parameter will then be :auth0.

You've already seen and learned about the Auth0 Client ID (auth0_client_id), Auth0 Client Secret (auth0_client_secret), and Auth0 Domain (auth0_domain). But, what are the callback_path and authorize_params parameters doing? The callback_path configured here will be passed as the redirect_uri query parameter to the /authorize endpoint, so then Auth0 will use it to redirect your users back to the Rails application. We'll cover more about these two parameters briefly.

Rails uses OmniAuth to trigger the authentication process whenever one of your users needs to log in. The OmniAuth Auth0 strategy kicks in, which makes Rails redirect the user to the Auth0 Universal Login page to carry out the authentication process. Upon successful authentication, Auth0 redirects the users to whatever Auth0 Callback URL you've set up.

OmniAuth sets a special "Authentication Hash" to /auth/:provider/callback on the Rack environment of a request. This hash contains information about the user based on the OmniAuth strategy in use that you can access under the key omniauth.auth. In the next section, you'll set up a controller's action to handle the Auth0 post-login behavior.

The authorize_params hash is used to verify the claims of the JWT token. A claim is a statement that a particular entity has a particular property, usually the user and their data.. The scope element inside the authorize_params hash refers to the OpenID Connect Scopes, which are used by your application during authentication to authorize access to a user's details, like name and picture.

Finally, the OmniAuth.config.on_failure statement allows your application to redirect you to a failure page in development mode when the authentication process fails. You'll later create the failure template.

For more information, you can refer to OmniAuth's documentation.

What about using scopes?

A property that you are configuring too for your OmniAuth-Auth0 strategy is the scope property. Here you're providing the OpenID Connect Scopes: openid profile email.

  • openid: This scope informs the Auth0 Authorization Server that the Client is making an OpenID Connect (OIDC) request to verify the user's identity. OpenID Connect is an authentication protocol.

  • profile: This scope value requests access to the user's default profile information, such as name, nickname, and picture.

  • email: This scope value requests access to the email and email_verified information.

The details of the OpenID Connect Scopes go into the ID Token. However, you can define custom API scopes to implement access control. You'll identify those custom scopes in the calls that your client applications make to that API. Auth0 includes API scopes in the access token as the scope claim value.

The concepts about API scopes or permissions are better covered in an Auth0 API tutorial such as "Use TypeScript to Create a Secure API with Node.js and Express: Role-Based Access Control".

Handle the Auth0 post-login behavior

The omniauth-auth0 gem already provides a POST /auth/auth0 endpoint to trigger the authentication process. However, in order to complete the authentication, process you will also need to create a Rails controller that handles the callback from the auth/auth0 endpoint as well as the logout process.

Remember earlier when you set up a callback_path as part of the OmniAuth configuration? This is where the dots connect. As mentioned above, your application needs to set up an endpoint that matches to the callback URL and then performs the necessary steps for your application to consider a user to be authenticated. In Rails, the callback endpoint will be a controller action. For modularization purposes, you will create a new controller to handle all the actions related to authentication.

Create a new controller:

COMMAND
bin/rails generate controller auth0 callback failure --skip-assets --skip-helper --skip-routes --skip-template-engine --skip-test-framework

and update it with the following content:

app/controllers/auth0_controller.rb
# frozen_string_literal: true
class Auth0Controller < ApplicationController
def callback
auth_info = request.env['omniauth.auth']
session[:credentials] = {}
session[:credentials][:id_token] = auth_info['credentials']['id_token']
redirect_to profile_path
end
def failure
@error_msg = request.params['message']
end
end

After a user attempts to log in, if it was successful, Auth0 will redirect them to the /auth/auth0/callback endpoint, and the router will call the callback action to handle the post-login behavior. If the authentication fails, the omniauth-auth0 gem redirects the user to the /auth/failure endpoint, which will dispatch your application's failure action. You could rename these actions to login instead of callback, for example, but to be consistent with the endpoint name you are going to leave it as callback for now.

In the callback action you are reading from the Authentication Hash, in this case, you'll only need the ID Token, a JWT token that when decoded, contains the user information.

Note authentication has already happened with the third party when the callback action is called, but usually, you need a way to define whether a user is authenticated or not at the application level. For that, you'll use Rails Sessions.

If the login is successful, you will store the user's ID Token in the session[:credentials][:id_token] hash and finally redirect to the /profile page. In Rails, the default Session Storage uses ActionDispatch::Session::CookieStore, which saves the session hash in a cookie on the client side. Rails CookieStore can only hold 4KB, so in order to prevent an ActionDispatch::Cookies::CookieOverflow error, let's change the configuration of the app to use the In-Memory store for development:

Create a new file config/initializers/session_store.rb

COMMAND
touch config/initializers/session_store.rb

and add the following content to it:

config/initializers/session_store.rb
Rails.application.config.session_store :cache_store

Then, go to config/environments/development.rb and update it as follows:

config/environments/development.rb
# frozen_string_literal: true
require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable server timing
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = true
config.cache_store = :memory_store
end
# Store uploaded files on the local file system (see config/storage.yml for options).
# config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
end

Create Auth0 Routes

You need to map the Auth0Controller actions to your Rails web application routes. Go to the config/routes.rb and create the following routes:

config/routes.rb
# frozen_string_literal: true
Rails.application.routes.draw do
root 'home#index'
scope :auth do
get 'failure' => 'auth0#failure'
# Auth0 routes
scope :auth0 do
get 'callback' => 'auth0#callback'
end
end
get 'protected', to: 'messages#protected'
get 'public', to: 'messages#public'
get 'admin', to: 'messages#admin'
get 'profile', to: 'users#profile'
match '/404', to: 'errors#not_found', via: :all
match '/500', to: 'errors#server_error', via: :all
end

Run bin/rails routes in your terminal, and you should see the newly created routes:

INFO
auth0_callback GET /auth/auth0/callback(.:format) auth0#callback
failure GET /auth/failure(.:format) auth0#failure

Create the failure page

As mentioned earlier, you need a failure page to render when the authentication process fails. This page will be the respective view to the /auth/failure action from your Auth0Controller.

Note you could have also handled the failure endpoint with a redirect to an existing page, but to showcase the error scenario, let's go ahead and create a failure view.

Create a new folder in app/views/ and then a new file app/views/auth0/failure.html.erb as follows:

COMMAND
mkdir app/views/auth0
touch app/views/auth0/failure.html.erb

Next, add the following content to the file:

app/views/auth0/failure.html.erb
<div class="content-layout">
<h1 id="page-title" class="content__title">Authentication Failure</h1>
<div class="content__body">
<p id="page-description">
<%= @error_msg %>
</p>
</div>
</div>

In this view, you are reading from the instance variable @error_mesg you defined in Auth0Controller#failure and showing it on the page.

Add User Login to Rails

The steps on how to build a Rails login form or login page are complex. You can save development time by using a login page hosted by Auth0 that has a built-in login form that supports different types of user authentication: username and password, social login, and Multi-Factor Authentication (MFA). You have created the routes and actions to handle the post-login behavior in your app. Now you just need to create a button that takes users from your Rails application to the login page.

Start by creating a new partial that will contain the authentication buttons:

COMMAND
touch app/views/layouts/partials/_navbar_buttons.html.erb

Next, fill it up with the following content:

app/views/layouts/partials/_navbar_buttons.html.erb
<div class="<%= nav_bar_class %>">
<%= button_to 'Log In', '/auth/auth0', params: {prompt: 'login'}, data: {turbo: "false"}, class: 'button__login' %>
</div>

In this partial, you are creating a button labeled "Log In". When the users click on this button, they will make a POST request to /auth/auth0, which is a route from the omniauth-auth0 gem. This action will redirect the user to the Universal Login page, as mentioned earlier.

You can send a number of params to the auth/auth0 endpoint, and they will be added as params to the Auth0's /authorize endpoint later on in order to customize the login experience.

Note you are passing data: {turbo: "false"} to the POST request. Rails uses Turbo by default. One of the features of Turbo consists of converting links form submissions into AJAX requests, which is supposed to speed up your applications thanks to Turbo Drive. In this case you'll want to disable Turbo for the buttons' form, so it's not an AJAX request.

Add user Sign-Up to Rails

Now your users are able to log in, but they also need to sign up. The process on how to build a Rails sign-up form is much more complex. However, you can use a sign-up form hosted by Auth0 that has a built-in password strength verification.

Let's create a button under the app/views/layouts/partials/_navbar_buttons.html.erb partial that takes users from your Rails application to the sign-up page in Auth0:

app/views/layouts/partials/_navbar_buttons.html.erb
<div class="<%= nav_bar_class %>">
<%= button_to 'Sign Up', '/auth/auth0', params: {prompt: 'login', screen_hint: 'signup'}, data: {turbo: "false"}, class: 'button__sign-up' %>
<%= button_to 'Log In', '/auth/auth0', params: {prompt: 'login'}, data: {turbo: "false"}, class: 'button__login' %>
</div>

Add User Logout to Rails

As mentioned earlier, to complete the authentication process, you need to log out your users from your Rails application and the Auth0 Authentication Server. To accomplish this task, you'll create a GET /auth/auth0/logout endpoint and map it to a Rails action in the Auth0Controller that you created in the preview steps.

Open the config/routes.rb file and include the logout endpoint:

config/routes.rb
# frozen_string_literal: true
Rails.application.routes.draw do
root 'home#index'
scope :auth do
get 'failure' => 'auth0#failure'
# Auth0 routes
scope :auth0 do
get 'callback' => 'auth0#callback'
get 'logout' => 'auth0#logout'
end
end
get 'protected', to: 'messages#protected'
get 'public', to: 'messages#public'
get 'admin', to: 'messages#admin'
get 'profile', to: 'users#profile'
match '/404', to: 'errors#not_found', via: :all
match '/500', to: 'errors#server_error', via: :all
end

Note that you didn't create a Login or a Sign-Up route within your app's routes. This is because these two routes are provided by the omniauth-auth0 gem.

Now, let's create the logout action in the app/controllers/auth0_controller.rb as follows:

app/controllers/auth0_controller.rb
# frozen_string_literal: true
class Auth0Controller < ApplicationController
def callback
auth_info = request.env['omniauth.auth']
session[:credentials] = {}
session[:credentials][:id_token] = auth_info['credentials']['id_token']
redirect_to profile_path
end
def failure
@error_msg = request.params['message']
end
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

The Auth0Controller#logout action uses the reset_session method to clear out all the objects stored within it. Then, it redirects the user to the route specified in the returnTo parameter in the logout_url method. Note you are setting allow_other_host: true. The reason you do this is that the logout_url is different from your host (localhost).

Let's create the Log Out button:

app/views/layouts/partials/_navbar_buttons.html.erb
<div class="<%= nav_bar_class %>">
<%= button_to 'Sign Up', '/auth/auth0?prompt=login&screen_hint=signup', method: :post, data: {turbo: "false"}, class: 'button__sign-up' %>
<%= button_to 'Log In', '/auth/auth0?prompt=login', method: :post, data: {turbo: "false"}, class: 'button__login' %>
<%= button_to 'Log Out', logout_path, method: :get, data: {turbo: "false"}, class: 'button__logout' %>
</div>

When you're implementing the logout functionality, there are typically three-session layers you need to consider:

  • The Application Session Layer, which is being cleared with the reset_session method.
  • The Auth0 Session Layer, which is being cleared by redirecting the user to the GET /v2/logout endpoint, and that's what you're doing by redirecting to the logout_url and providing the query parameters client_id and returnTo.
  • The Identity Provider Session Layer, which is not necessary to do here.

Now that you have added all the buttons, you should render them depending if the user is logged in or not.

Render Content Based on Authentication

In this section, you'll learn how to render content conditionally based on the authentication status of your users. Since you are storing the user's ID Token in the session, you can use this value to decide whether a user is authenticated or not.

Render the authentication buttons conditionally

The Rails starter web application features a desktop and mobile navigation experience.

When using your Rails web application on a viewport large enough to fix a desktop or tablet experience, you'll see a navigation bar at the top of the page.

When using a viewport that fits the screen constraints of a mobile device, you'll see a menu button at the top-right corner of the page. Tapping or clicking on the menu button opens a modal that shows you the different pages that you can access in the application.

In this section, you'll expose the button elements that trigger the login, sign-up, and logout events through these page navigation elements.

You'll show both the login and sign-up buttons on the navigation bar when the user is not logged in. Naturally, you'll show the logout button when when the user is logged in. Open the app/views/layouts/partials/_navbar_buttons.html.erb file and add the following code:

app/views/layouts/partials/_navbar_buttons.html.erb
<div class="<%= nav_bar_class %>">
<% unless session['credentials'] %>
<%= button_to 'Sign Up', '/auth/auth0?prompt=login&screen_hint=signup', method: :post, data: {turbo: "false"}, class: 'button__sign-up' %>
<%= button_to 'Log In', '/auth/auth0?prompt=login', method: :post, data: {turbo: "false"}, class: 'button__login' %>
<% else %>
<%= button_to 'Log Out', logout_path, method: :get, data: {turbo: "false"}, class: 'button__logout' %>
<% end %>
</div>

Next, you need to add the app/views/layouts/partials/_navbar_buttons.html.erb partial into the desktop and mobile navigation bars so the authentication buttons are rendered. Let's start with the desktop navigation user experience. Go to app/views/layouts/partials/_navbar.html.erb and add the following line:

app/views/layouts/partials/_navbar.html.erb
<div class="nav-bar__container">
<nav class="nav-bar">
<div class="nav-bar__brand">
<%= link_to root_path do %>
<img
class="nav-bar__logo"
src="https://cdn.auth0.com/blog/hub/code-samples/hello-world/auth0-logo.svg"
alt="Auth0 shield logo"
width="122"
height="36"
/>
<% end %>
</div>
<%= render partial: "layouts/partials/navbar_tabs", locals: { nav_bar_class: "nav-bar__tabs", nav_bar_tab_class: "nav-bar__tab" } %>
<%= render partial: "layouts/partials/navbar_buttons", locals: { nav_bar_class: "nav-bar__buttons" } %>
</nav>
</div>

The mobile navigation experience works in the same fashion, except that the authentication-related buttons are tucked into the mobile menu modal. Go to the app/views/layouts/partials/mobile/_navbar.html.erb and add the following:

app/views/layouts/partials/mobile/_navbar.html.erb
<div class="mobile-nav-bar__container">
<nav id="mobile-nav-bar" class="mobile-nav-bar">
<div class="mobile-nav-bar__brand">
<%= link_to root_path do %>
<img
class="mobile-nav-bar__logo"
src="https://cdn.auth0.com/blog/hub/code-samples/hello-world/auth0-logo.svg"
alt="Auth0 shield logo"
width="82"
height="24"
/>
<% end %>
</div>
<span class="mobile-nav-bar__toggle material-icons" id="mobile-menu-toggle-button">
menu
</span>
<div class="mobile-nav-bar__menu mobile-nav-bar__menu--closed" id="mobile-menu">
<%= render partial: "layouts/partials/navbar_tabs", locals: { nav_bar_class: "mobile-nav-bar__tabs", nav_bar_tab_class: "mobile-nav-bar__tab" } %>
<%= render partial: "layouts/partials/navbar_buttons", locals: { nav_bar_class: "mobile-nav-bar__buttons" } %>
</div>
</nav>
</div>

Go ahead and try to log in. Your Rails application redirects you to the Auth0 Universal Login page. You can use the form to log in with a username and password or a social identity provider like Google. Notice that this login page also gives you the option to sign up.

New Auth0 Universal Login Experience Form

However, when you click the sign-up button from your application directly, Rails takes you to the Signup page, where your users can sign up for the Rails application. Try it out!

New Auth0 Universal Login Experience Signup Page

Using the Auth0 Signup feature requires you to enable the Auth0 New Universal Login Experience in your tenant.

Open the Universal Login section of the Auth0 Dashboard and choose the "New" option under the "Experience" subsection.

Scroll down and click on the "Save Changes" button.

You can customize the appearance of New Universal Login pages. You can also override any text in the New Experience using the Text Customization API.

Render navigation tabs conditionally

There may be use cases where you want to hide user interface elements from users who have not logged in to your application. For this starter application, only authenticated users should see the navigation tabs to access the /protected and /admin pages.

To implement this use case, you'll rely once again on the session['credentials'] value from the current session.

Open the app/views/layouts/partials/_navbar_tabs.html.erb file and add the following code:

app/views/layouts/partials/_navbar_tabs.html.erb
<div class="<%= nav_bar_class %>">
<%= link_to 'Profile', profile_path, class: nav_bar_tab_class %>
<%= link_to 'Public', public_path, class: nav_bar_tab_class %>
<% if session['credentials'] %>
<%= link_to 'Protected', protected_path, class: nav_bar_tab_class %>
<%= link_to 'Admin', admin_path, class: nav_bar_tab_class %>
<% end %>
</div>

For the navigation tabs and buttons, you are checking if the session['credentials'] exists, which means the user is authenticated.

Log out from your Rails web application and notice how now you can only see the tabs for the /profile and /public pages in the navigation bar, along with the login and sign-up buttons. Log in and then see the rest of the navigation bar show up.

Keep in mind that this does not restrict access to the /admin and /protected pages at all. You'll learn how to protect Rails web application routes in the next section.

Restrict access to Rails web application routes

Now that the navigation tabs will render conditionally, you'll need to make sure that only authenticated users have access to certain routes.

To do so, let's go to the ApplicationController in app/controllers/application_controller.rb and add the following content:

app/controllers/application_controller.rb
# frozen_string_literal: true
class ApplicationController < ActionController::Base
def require_login
return if current_user
redirect_post('/auth/auth0', params: { prompt: 'login' },
options: { method: :post, authenticity_token: 'auto' })
end
def current_user
decoded_id_token if session[:credentials]
end
def decoded_id_token
JWT.decode(session[:credentials][:id_token], nil, false)[0].deep_symbolize_keys
end
end

The require_login method is the one you'll use to protect access to your actions. To do so, it checks for a current_user, and if there is not any, it will make a POST request to the internal endpoint that the SDK provides us, /auth/auth0, which will trigger the authentication flow to redirect users to the Universal login page. The POST redirect is made using an external gem called repost.

The current_user method checks if there is an ID Token stored in the session hash, and it calls the decoded_id_token method, which uses the JWT gem to decode the id_token. You will use this method later to dump the profile information.

You need to install the repost and jwt gems, so let's go ahead and do so.

In your Gemfile add the following:

Gemfile
# Gem Repost implements Redirect using POST method. https://vergilet.github.io/repost/
gem 'repost'
# A ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard. https://github.com/jwt/ruby-jwt
gem 'jwt'

Run bundle install in your terminal to install them:

COMMAND
bundle install

Now that you have implemented the require_login method in the ApplicationController, you can secure your controller's actions to ensure only authenticated users can access certain routes.

You'll need to make sure that:

  • Only authenticated users have access to UsersController#profile.
  • Only authenticated users have access to MessagesController#protected and MessagesController#admin.

Secure the Profile

Go to app/controllers/users_controller.rb and replace its content with the following:

app/controllers/users_controller.rb
# frozen_string_literal: true
class UsersController < ApplicationController
before_action :require_login
def profile
@user = User.new(
nickname: 'Customer',
name: 'One Customer',
picture: 'https://cdn.auth0.com/blog/hello-auth0/auth0-user.png',
updated_at: '2021-05-04T21:33:09.415Z',
email_verified: false,
sub: 'auth0|12345678901234567890'
)
end
end

By using the before_action filter require_login from the ApplicationController, you'll ensure the user is authenticated when they try to go to any action in the UsersController.

Secure Protected and Admin Messages

Go to app/controllers/messages_controller.rb and add the following line:

app/controllers/messages_controller.rb
# frozen_string_literal: true
class MessagesController < ApplicationController
before_action :require_login, except: [:public]
# ...
end

Here the before_acion filter ensures the user is authenticated when they try to access any of the controller's actions except the public action, enforcing authentication to access the /protected or /admin routes.

You can now test that these guarded paths require users to log in before accessing them. Log out and try to access the Profile page, Protected page, or the Admin page. If it works, Rails redirects you to log in with Auth0.

Once you log in, Rails should take you to the /profile page as specified by the Auth0Controller#callback action that handles the post-login behavior.

Client-side guards improve the user experience of your web application, not its security.

In Security StackExchange, Conor Mancone explains that server-side guards are about protecting data while client-side guards are about improving user experience.

The main takeaways from his response are:

  • You can't rely on client-side restrictions, such as navigation guards and protected routes, to protect sensitive information.
    • Attackers can potentially get around client-side restrictions.
  • Your server should not return any data that a user should not access. The wrong approach is to return all the user data from the server and let the front-end framework decide what to display and what to hide based on the user authentication status.
    • Anyone can open the browser's developer tools and inspect the network requests to view all the data.
  • The use of navigation guards helps improve user experience, not user security.
    • Without guards, a user who has not logged in may wander into a page with restricted information and see an error like "Access Denied".
    • With guards that match the server permissions, you can prevent users from seeing errors by preventing them from visiting the restricted page.

Retrieve User Profile Information

After a user successfully logs in, Auth0 sends an ID token to your Rails web application. Authentication systems, such as Auth0, use ID Tokens in token-based authentication to cache user profile information and provide it to a client application. The caching of ID tokens can improve the performance and responsiveness of your Rails web application.

You can decode the ID Token with the JWT gem into a hash and show it in the profile view to personalize the user interface of your Rails web application. Some of the ID token information includes the name, nickname, picture, and email of the logged-in user.

Let's go to the UsersController and add the following code:

app/controllers/users_controller.rb
# frozen_string_literal: true
class UsersController < ApplicationController
before_action :require_login
def profile
@user = User.new(user_params)
end
private
def user_params
current_user.slice(:nickname, :name, :picture, :updated_at, :email, :email_verified, :sub)
end
end

If the user is authenticated, it'll be stored in the instance variable @user from the current_user. Internally, the current_user method from the ApplicationController uses the ID Token and decodes it to get the user information.

Let's update the app/views/users/profile.html.erb view, so it shows the decoded ID token as follows:

app/views/users/profile.html.erb
<div class="content-layout">
<h1 id="page-title" class="content__title">Profile Page</h1>
<div class="content__body">
<p id="page-description">
<span>
You can use the <strong>ID Token</strong> to get the profile information
of an authenticated user.
</span>
<span>
<strong>Only authenticated users can access this page.</strong>
</span>
</p>
<div class="profile-grid">
<div class="profile__header">
<%= image_tag("#{@user.picture}", class: "profile__avatar", alt: "Profile") %>
<div class="profile__headline">
<h2 class="profile__title"><%= @user.name %></h2>
<span class="profile__description"><%= @user.email %></span>
</div>
</div>
<div class="profile__details">
<%= render partial: "layouts/partials/code_snippet", locals: { title: "Decoded ID Token", code: @user.pretty_json } %>
</div>
</div>
</div>
</div>

Since the user model inherits from ApplicationModel, you can make use of the pretty_json method implemented in the starter project.

Integrate Rails with a protected API Server

This section focuses on showing you how to get an access token in your Rails Web App and how to use it to make API calls to protected API endpoints.

When you use Auth0, you delegate the authentication process to a centralized service. Auth0 provides you with functionality to log in and log out users from your Rails web application. However, your application may need to access protected resources from an API.

You can also protect an API with Auth0. There are multiple API quickstarts to help you integrate Auth0 with your backend platform.

When you use Auth0 to protect your API, you also delegate the authorization process to a centralized service that ensures only approved client applications can access protected resources on behalf of a user.

Retrieve an Access Token from Auth0

Your Rails web application authenticates the user and receives an access token from Auth0. The application can then pass that access token to your API as a credential. In turn, your API can use Auth0 libraries to verify the access token it receives from the calling application and issue a response with the desired data.

Let's edit the Auth0Controller#callback action and add the following:

app/controllers/auth0_controller.rb
# frozen_string_literal: true
class Auth0Controller < ApplicationController
def callback
auth_info = request.env['omniauth.auth']
session[:credentials] = {}
session[:credentials][:id_token] = auth_info['credentials']['id_token']
session[:credentials][:access_token] = auth_info['credentials']['token']
redirect_to profile_path
end
...
end

Similar to what you did to store the ID Token, you are storing the Access Token in the session hash so it can be available to use to make authenticated requests to the external API.

How can you make secure API calls from a Rails Web App?

Instead of creating an API from scratch to test the authentication and authorization flow between the client and the server, you can pair this client application with an API server that matches the technology stack you use at work. The Rails "Hello World" client application that you have been building up can interact with any of the "Hello World" API server samples from the Auth0 Developer Center.

Pick an API code sample in your preferred backend framework and language from the list below and follow the instructions on the code sample page to set it up. Once you complete the sample API server setup, please return to this page to learn how to integrate that API server with your Rails web application.

actix-web
rust
Actix Web/Rust API:Authorization Code Sample
Code sample of a simple Actix Web server that implements token-based authorization using Auth0.
aspnet-core
csharp
ASP.NET Core Code Sample:Web API Authorization
Code sample of a simple ASP.NET Core server that implements token-based authorization using Auth0.
django
python
Django/Python API:Authorization Code Sample
Code sample of a simple Django server that implements token-based authorization using Auth0.
express
javascript
Express.js Code Sample:Basic API Authorization
Code sample of a simple Express.js server that implements token-based authorization using Auth0.
express
typescript
Express.js/TypeScript Code Sample:Basic API Authorization
Code sample of a simple Express.js server built with TypeScript that implements token-based authorization using Auth0.
fastapi
python
FastAPI/Python Code Sample:Basic API Authorization
Code sample of a simple FastAPI server that implements token-based authorization using Auth0.
flask
python
Flask/Python API:Authorization Code Sample
Code sample of a simple Flask server that implements token-based authorization using Auth0.
laravel
php
Laravel/PHP API:Authorization Code Sample
Code sample of a simple Laravel server that implements token-based authorization using Auth0.
lumen
php
Lumen/PHP API:Authorization Code Sample
Code sample of a simple Lumen server that implements token-based authorization using Auth0.
phoenix
elixir
Phoenix/Elixir API:Authorization Code Sample
Code sample of a simple Phoenix server that implements token-based authorization using Auth0.
rails
ruby
Ruby on Rails API:Authorization Code Sample
Code sample of a simple Rails server that implements authorization using Auth0.
spring
java
Spring Code Sample:Basic API Authorization
Java code sample that implements token-based authorization in a Spring Web API server to protect API endpoints, using Spring Security.
spring
java
Spring Functional Code Sample:Basic API Authorization
Java code sample that implements token-based authorization in a Spring Web API server to protect API endpoints, following a functional approach.
spring-webflux
java
Spring WebFlux Code Sample:Basic API Authorization
Java code sample that implements token-based authorization in a Spring WebFlux API server to protect API endpoints, using Spring Security.
standard-library
golang
Golang Code Sample:Basic API Authorization
Code sample of a simple Golang server that implements token-based authorization using Auth0.
symfony
php
Symfony Code Sample:Basic API Authorization
Code sample of a simple Symfony server that implements token-based authorization using Auth0.

Call a Protected API from Rails

Once you have set up the API server code sample, you should have created an Auth0 Audience value. Store that value in the following field so that you can use it throughout the instructions presented on this page easily:

Now, open your .env file and add the following variables to it:

.env
PORT=4040
SECRET_KEY_BASE=KEY-VALUE
AUTH0_CALLBACK_PATH=/auth/auth0/callback
AUTH0_DOMAIN=AUTH0-DOMAIN
AUTH0_CLIENT_ID=AUTH0-CLIENT-ID
AUTH0_CLIENT_SECRET=AUTH0-CLIENT-SECRET
AUTH0_AUDIENCE=AUTH0-AUDIENCE
API_SERVER_URL=http://localhost:6060

You are using AUTH0_AUDIENCE to add the value of your Auth0 API Audience so that your Rails web client application can request resources from the API that such audience value represents.

Let's understand better what the AUTH0_AUDIENCE and API_SERVER_URL values represent.

The API_SERVER_URL is the URL where your sample API server listens for requests. In production, you'll change this value to the URL of your live server.

Your Rails web application must pass an access token when it calls a target API to access protected resources. You can request an access token in a format that the API can verify by passing the audience to the authorize_params in your OmniAuth-Auth0 configuration.

The value of the Auth0 Audience must be the same for both the Rails web client application and the API server you decided to set up.

Why is the Auth0 Audience value the same for both apps? Auth0 uses the value of the audience prop to determine which resource server (API) the user is authorizing your Rails web application to access. It's like a phone number. You want to ensure that your Rails web application "texts the right API".

Now let's add the audience to the Auth0 configuration in the config/auth0.yml file:

config/auth0.yml
development:
auth0_domain: <%= ENV['AUTH0_DOMAIN'] %>
auth0_client_id: <%= ENV['AUTH0_CLIENT_ID'] %>
auth0_client_secret: <%= ENV['AUTH0_CLIENT_SECRET'] %>
auth0_callback_path: <%= ENV['AUTH0_CALLBACK_PATH'] %>
auth0_audience: <%= ENV['AUTH0_AUDIENCE'] %>

Now in the config/initializers/auth0.rb, you'll call the newly created configuration for the audience as follows:

config/initializers/auth0.rb
# frozen_string_literal: true
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: AUTH0_CONFIG['auth0_callback_path'],
authorize_params: {
audience: AUTH0_CONFIG['auth0_audience'],
scope: 'openid profile email'
}
)
end

Create an External API provider

Let's create a new folder providers under the app directory and inside a new file called external_api_provider.rb. This file is in charge of making an authenticated request to an external API to get an admin message and returning the response to the MessageService.

Run the following commands in your terminal to create them:

COMMAND
mkdir app/providers
touch app/providers/external_api_provider.rb

Next, populate the file you just created with the following code:

app/providers/external_api_provider.rb
# frozen_string_literal: true
class ExternalApiProvider
def self.get_admin_message(access_token)
uri = URI("#{ENV.fetch('API_SERVER_URL', nil)}/api/messages/admin")
api_response = Net::HTTP.get_response(
uri,
{ 'Authorization' => "Bearer #{access_token}" }
)
parsed_response = JSON.parse(api_response.body).deep_symbolize_keys
api_response.code == '200' ? Message.new(text: parsed_response[:text]) : Error.new(message: parsed_response[:message])
end
end

The get_admin_message method takes an access token as an argument and makes an authenticated call to the api/messages/admin endpoint of the API server. Finally, it retrieves the external API response to populate the message.

Note there is a new model Error. Let's go ahead and create it:

COMMAND
touch app/models/error.rb

Next, fill the Error Model with the following content:

app/models/error.rb
# frozen_string_literal: true
class Error < ApplicationModel
attr_accessor :message
end

The ExternalApiProvider will be called from the MessagesService. Let's adapt it to handle the External API response:

Go to app/services/messages_service.rb and replace its content with the following code:

app/services/messages_service.rb
# frozen_string_literal: true
class MessagesService
def self.call(message_type, access_token = nil)
if message_type == Message::ADMIN
ExternalApiProvider.get_admin_message(access_token)
else
Message.new(text: "This is a #{message_type} message.")
end
end
end

You are now passing an access_token argument to the call method so it can be used by the ExternalAPI.get_admin_message method.

Note you only want to call the External API if it's an admin message, in which case you'll create a new Message object with the API's response and use it as the text attribute. For any other message type, you'll generate a static message.

Now let's pass the access token when calling the MessagesService.

Update the app/controllers/messages_controller.rb file as follows:

app/controllers/messages_controller.rb
# frozen_string_literal: true
class MessagesController < ApplicationController
before_action :require_login, except: [:public]
def public
@message = MessagesService.call(Message::PUBLIC)
end
def protected
@message = MessagesService.call(Message::PROTECTED)
end
def admin
@message = MessagesService.call(Message::ADMIN, session[:credentials][:access_token])
end
end

You are getting the Access Token from the session hash, which you previously stored after the user authenticated in the Auth0Controller#callback method. Note you can safely use session[:credentials][:access_token] here because of the before_action filter, which guarantees the user has authenticated before accessing this action. Otherwise, the MessagesService will pass a nil Access Token to the ExternalApiProvider, and you'll get a { message: Permission Denied } error from the External API.

Adapt the Message Model

Let's clean up the Message model and replace its content with the following:

app/models/message.rb
# frozen_string_literal: true
class Message < ApplicationModel
attr_accessor :text
ADMIN = 'admin'
PROTECTED = 'protected'
PUBLIC = 'public'
end

Adapt the admin message view

Let's make a small change to the admin message view by implicitly saying the message comes from an external API:

admin.html.erb
<div class="content-layout">
<h1 id="page-title" class="content__title">Admin Page</h1>
<div class="content__body">
<p id="page-description">
<span>
This page retrieves an <strong>admin message</strong> from an external API.
</span>
<span>
<strong>Only authenticated users with the <code>read:admin-messages</code> permission should access this page.</strong>
</span>
</p>
<%= render partial: "layouts/partials/code_snippet", locals: { title: "Admin Message", code: @message.pretty_json } %>
</div>
</div>

Conclusion

You have implemented user authentication in Ruby on Rails to identify your users, get user profile information, and control the content that your users can access by protecting routes and API resources.

This guide covered the most common authentication use case for a Rails Web application: simple login and logout. However, Auth0 is an extensible and flexible identity platform that can help you achieve even more. If you have a more complex use case, check out the Auth0 Architecture Scenarios to learn more about the typical architecture scenarios we have identified when working with customers on implementing Auth0.

We'll cover advanced authentication patterns and tooling in future guides, such as using a pop-up instead of redirecting users to log in, adding permissions to ID tokens, using metadata to enhance user profiles, and much more.