Rails Authentication By Example
Updated on January 30, 2023The Rails examples in this guide will help you learn how to implement the following security concepts using Auth0 by Okta:
- 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.5
- 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 by Okta, 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:
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:
cd web-app_rails_ruby_hello-world
Install the Rails project dependencies as follows:
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:
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:
touch .env
Then, populate the .env
file with the following environment variables:
PORT=4040SECRET_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:
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:
- Auth0 Universal Login for Web, iOS & Android.
- Up to 2 social identity providers like Google, GitHub, and Twitter.
- Up to 3 Actions, Rules, & Hooks to customize and extend Auth0's capabilities.
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:
Auth0 Rails Web App Code Sample
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:
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.
http://localhost:4040
The above value is the URL that Auth0 can use to redirect your users after they log out.
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.
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:
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:
Now, update the .env
file under the Rails project directory as follows:
PORT=4040SECRET_KEY_BASE=KEY-VALUEAUTH0_CALLBACK_PATH=/auth/auth0/callbackAUTH0_DOMAIN=AUTH0-DOMAINAUTH0_CLIENT_ID=AUTH0-CLIENT-IDAUTH0_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.
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:
# frozen_string_literal: truesource '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 gemgem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]# Reduces boot times through caching; required in config/boot.rbgem 'bootsnap', require: false# An OmniAuth strategy to log in with Auth0.gem 'omniauth-auth0', '~> 3.0'# Prevents forged authentication requestsgem 'omniauth-rails_csrf_protection', '~> 1.0'group :development, :test do# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gemgem 'debug', platforms: %i[mri mingw x64_mingw]# ENV variables management https://github.com/bkeepers/dotenvgem 'dotenv-rails'gem 'pry'endgroup :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:
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:
touch config/auth0.yml
Populate the config/auth0.yml
file with the following content:
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:
touch config/initializers/auth0.rb
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:
# frozen_string_literal: trueAUTH0_CONFIG = Rails.application.config_for(:auth0)Rails.application.config.middleware.use OmniAuth::Builder doprovider(: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'})endOmniAuth.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.
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 asname
,nickname
, andpicture
. -
email
: This scope value requests access to theemail
andemail_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.
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:
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:
# frozen_string_literal: trueclass Auth0Controller < ApplicationControllerdef callbackauth_info = request.env['omniauth.auth']session[:credentials] = {}session[:credentials][:id_token] = auth_info['credentials']['id_token']redirect_to profile_pathenddef failure@error_msg = request.params['message']endend
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
touch config/initializers/session_store.rb
and add the following content to it:
Rails.application.config.session_store :cache_store
Then, go to config/environments/development.rb
and update it as follows:
# frozen_string_literal: truerequire '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 timingconfig.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 = trueconfig.action_controller.enable_fragment_cache_logging = trueconfig.public_file_server.headers = {'Cache-Control' => "public, max-age=#{2.days.to_i}"}elseconfig.action_controller.perform_caching = trueconfig.cache_store = :memory_storeend# 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 = falseconfig.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:
# frozen_string_literal: trueRails.application.routes.draw doroot 'home#index'scope :auth doget 'failure' => 'auth0#failure'# Auth0 routesscope :auth0 doget 'callback' => 'auth0#callback'endendget '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: :allmatch '/500', to: 'errors#server_error', via: :allend
Run bin/rails routes
in your terminal, and you should see the newly created routes:
auth0_callback GET /auth/auth0/callback(.:format) auth0#callbackfailure 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:
mkdir app/views/auth0touch app/views/auth0/failure.html.erb
Next, add the following content to the file:
<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:
touch app/views/layouts/partials/_navbar_buttons.html.erb
Next, fill it up with the following content:
<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:
<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:
# frozen_string_literal: trueRails.application.routes.draw doroot 'home#index'scope :auth doget 'failure' => 'auth0#failure'# Auth0 routesscope :auth0 doget 'callback' => 'auth0#callback'get 'logout' => 'auth0#logout'endendget '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: :allmatch '/500', to: 'errors#server_error', via: :allend
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:
# frozen_string_literal: trueclass Auth0Controller < ApplicationControllerdef callbackauth_info = request.env['omniauth.auth']session[:credentials] = {}session[:credentials][:id_token] = auth_info['credentials']['id_token']redirect_to profile_pathenddef failure@error_msg = request.params['message']enddef logoutreset_sessionredirect_to logout_url, allow_other_host: trueendprivatedef logout_urlrequest_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_sendend
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:
<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 thelogout_url
and providing the query parametersclient_id
andreturnTo
. - 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 the user is logged in. Open the app/views/layouts/partials/_navbar_buttons.html.erb
file and add the following code:
<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:
<div class="nav-bar__container"><nav class="nav-bar"><div class="nav-bar__brand"><%= link_to root_path do %><imgclass="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:
<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 %><imgclass="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.
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!
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.
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:
<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:
# frozen_string_literal: trueclass ApplicationController < ActionController::Basedef require_loginreturn if current_userredirect_post('/auth/auth0', params: { prompt: 'login' },options: { method: :post, authenticity_token: 'auto' })enddef current_userdecoded_id_token if session[:credentials]enddef decoded_id_tokenJWT.decode(session[:credentials][:id_token], nil, false)[0].deep_symbolize_keysendend
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:
# 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-jwtgem 'jwt'
Run bundle install
in your terminal to install them:
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
andMessagesController#admin
.
Secure the Profile
Go to app/controllers/users_controller.rb
and replace its content with the following:
# frozen_string_literal: trueclass UsersController < ApplicationControllerbefore_action :require_logindef 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: 'customer@example.com',email_verified: false,sub: 'auth0|12345678901234567890')endend
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:
# frozen_string_literal: trueclass MessagesController < ApplicationControllerbefore_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:
# frozen_string_literal: trueclass UsersController < ApplicationControllerbefore_action :require_logindef profile@user = User.new(user_params)endprivatedef user_paramscurrent_user.slice(:nickname, :name, :picture, :updated_at, :email, :email_verified, :sub)endend
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:
<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 informationof 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.
Authentication Beyond Passwords: Try Passkeys Today
So far, you have seen how a user can sign up or log in to your application with a username and password. However, you can free your users from having to remember yet another password by allowing them to use passkeys as a new way to log in.
Passkeys are a phishing-resistant alternative to traditional authentication factors, such as the username/password combo, that offer an easier and more secure login experience to users.
You don't have to write any new code to start using passkeys in your application. You can follow the "Authentication with Passkeys" lab to learn how to enable passkeys in your Auth0 tenant and learn more about this emerging technology. Once you complete that optional lab, you can come back to this guide to continue learning about how to access protected API resources on behalf of a user from your application.
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:
# frozen_string_literal: trueclass Auth0Controller < ApplicationControllerdef callbackauth_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_pathend...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.
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:
PORT=4040SECRET_KEY_BASE=KEY-VALUEAUTH0_CALLBACK_PATH=/auth/auth0/callbackAUTH0_DOMAIN=AUTH0-DOMAINAUTH0_CLIENT_ID=AUTH0-CLIENT-IDAUTH0_CLIENT_SECRET=AUTH0-CLIENT-SECRETAUTH0_AUDIENCE=AUTH0-AUDIENCEAPI_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:
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:
# frozen_string_literal: trueAUTH0_CONFIG = Rails.application.config_for(:auth0)Rails.application.config.middleware.use OmniAuth::Builder doprovider(: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:
mkdir app/providerstouch app/providers/external_api_provider.rb
Next, populate the file you just created with the following code:
# frozen_string_literal: trueclass ExternalApiProviderdef 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_keysapi_response.code == '200' ? Message.new(text: parsed_response[:text]) : Error.new(message: parsed_response[:message])endend
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:
touch app/models/error.rb
Next, fill the Error
Model with the following content:
# frozen_string_literal: trueclass Error < ApplicationModelattr_accessor :messageend
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:
# frozen_string_literal: trueclass MessagesServicedef self.call(message_type, access_token = nil)if message_type == Message::ADMINExternalApiProvider.get_admin_message(access_token)elseMessage.new(text: "This is a #{message_type} message.")endendend
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:
# frozen_string_literal: trueclass MessagesController < ApplicationControllerbefore_action :require_login, except: [:public]def public@message = MessagesService.call(Message::PUBLIC)enddef protected@message = MessagesService.call(Message::PROTECTED)enddef admin@message = MessagesService.call(Message::ADMIN, session[:credentials][:access_token])endend
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:
# frozen_string_literal: trueclass Message < ApplicationModelattr_accessor :textADMIN = '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:
<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.