rails logo
ruby logo

Rails API Authorization By Example

Updated on January 30, 2023
Photo of Carla Urrea Stabile
Carla Urrea StabileSenior Developer Advocate

This Ruby guide will help you learn how to secure a Rails API using token-based authorization. You'll learn how to integrate Auth0 by Okta with Ruby on Rails to implement the following security features:

  • Use Rails Concerns to enforce API security policies.
  • Perform access control in Rails using a token-based authorization strategy powered by JSON Web Tokens (JWTs).
  • Validate access tokens in JSON Web Token (JWT) format using a Rails helper class.
  • Make authenticated requests to a secure Rails API server.

Optionally, you can also learn how to implement Role-Based Access Control (RBAC) in Rails by checking permissions associated with roles before the Rails API can serve an incoming request.

How Does Auth0 Work?

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 client applications 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 to your client app, returning an access token. The client application can then use that access token to authenticate itself with your Rails API and access protected resources in your server or database layers.

Get the Starter Project

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

  • Ruby 3.1.2
  • Rails 7.0.4
  • Bundler 2.3.19

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

COMMAND
git clone -b starter https://github.com/auth0-developer-hub/api_rails_ruby_hello-world.git

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

COMMAND
cd api_rails_ruby_hello-world

Install the Rails project dependencies by running the following command:

COMMAND
bundle install

The starter Rails project defines API endpoints to let client applications access a simple message resource:

ENDPOINTS
# get a public message
GET /api/messages/public
# get a protected message
GET /api/messages/protected
# get an admin message
GET /api/messages/admin

The starter project does not implement access control for these endpoints. After you follow the steps in this guide, the API server will require each request to have a valid access token on their authorization header to access GET /api/messages/protected and GET /api/messages/admin. GET /api/messages/public would be the only public endpoint. Later, you'll integrate this Rails API with an actual client application using a frontend technology of your choice.

You'll run your Rails server on port 6060 locally. The compatible demo client application runs on http://localhost:4040 by default. However, this Rails starter application uses CORS to restrict which origins can request its resources.

You'll manage these configuration elements of your Rails application using environment variables. You'll store those values in a .env file, and you'll access them using the dotenv-rails gem.

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

COMMAND
touch .env

Populate your .env file with the following content:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040

The PORT environment variable defines the port in which your Rails API server will run: http://localhost:6060. The CLIENT_ORIGIN_URL variable defines the origin from which a browser can load resources from your Rails API — other than your API's own origin.

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

COMMAND
bin/rails s

You are ready to start implementing authorization in this Rails project. First, you'll need to configure the Rails API server to connect successfully to Auth0. Afterward, you'll use a Rails Concern to validate bearer tokens from incoming API requests.

Configure a Ruby on Rails API with Auth0

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

Sign up and create an Auth0 API

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. Open the "APIs" section of the Auth0 Dashboard.

Then, click the "Create API" button. A modal opens with a form to provide a name for the API registration and define an API Identifier. Use the following values:

Name
Auth0 Rails Code Sample
Identifier
https://hello-world.example.com

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

View 'Register APIs' document for more details.

A page loads up, presenting all the details about your application register with Auth0.

Next, locate the "General Settings" section.

Store the "Identifier" value in the following fields to set up your Rails API server in the next section:

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

In this guide, you'll implement token-based authorization. That is, your Rails API server will protect an endpoint by requiring that each request to that endpoint contains a valid access token.

The issuer of those access tokens will be an Auth0 authorization server. Your Rails API server needs to validate that the access token on a request comes from Auth0.

You'll need the Auth0 Domain and Auth0 Audience values to validate the access tokens.

When setting up APIs in the Auth0 Dashboard, we also refer to the API identifier as the Audience value, which you have already set up in the previous section.

Get the Auth0 domain

Now, follow these steps to get the Auth0 Domain value.

  • Open the Auth0 Domain Settings

  • Locate the bold text in the page description that follows this pattern: tenant-name.region.auth0.com. That's your Auth0 domain!

  • Paste the Auth0 domain value in the following field so that you can use it in the next section to set up your API server:

The region subdomain (au, us, or eu) is optional. Some Auth0 Domains don't have it.

Get an Auth0 access token

You'll also need a test access token to practice making secure calls to your API from a terminal application.

You can get a test access token from the Auth0 Dashboard by following these steps:

  • Head back to the Auth0 registration page of your Rails API and click on the "Test" tab.
If this is the first time that you are setting up a testing application, click on the "Create & Authorize Test Application" button.
  • Locate the section called "Response" and click on the copy button in the top-right corner.

  • Paste the access token value in the following field so that you can use it in the next sections to test your API 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.

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.

Define the Auth0 Environment Variables

Update the .env file under the project directory as follows to integrate the Auth0 Domain and Auth0 Audience values with your Rails application:

.env
PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=AUTH0-AUDIENCE
AUTH0_DOMAIN=AUTH0-DOMAIN

Once you reach the "Request Rails API Resources From a Client App" section of this guide, you'll learn how to use CLIENT_ORIGIN_URL along with an Auth0 Audience value to request protected resources from your Rails API using a client application that is also protected by Auth0.

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:
domain: <%= ENV.fetch('AUTH0_DOMAIN') %>
audience: <%= ENV.fetch('AUTH0_AUDIENCE') %>
production:
domain: <%= ENV.fetch('AUTH0_DOMAIN') %>
audience: <%= ENV.fetch('AUTH0_AUDIENCE') %>

Next, you'll use Rails::Application.config_for to load the config/auth0.yml configuration file. Locate the config/application.rb file and update its content with the following:

config/application.rb
# frozen_string_literal: true
require_relative 'boot'
require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
require 'active_job/railtie'
# require "active_record/railtie"
# require "active_storage/engine"
require 'action_controller/railtie'
require 'action_mailer/railtie'
# require "action_mailbox/engine"
# require "action_text/engine"
require 'action_view/railtie'
require 'action_cable/engine'
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module ApiRailsRubyHelloWorld
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.exceptions_app = routes
config.auth0 = config_for(:auth0)
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'deny',
'X-XSS-Protection' => '0',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options' => 'nosniff',
'Cache-Control' => 'no-store',
'Pragma' => 'no-cache',
'Content-Security-Policy' => "default-src 'self', frame-ancestors 'none'"
}
end
end

config_for is convenience method for loading config/auth0.yml for the current Rails environment.

config.auth0 = config_for(:auth0)

This Rails custom configuration allows you to use Rails.configuration.auth0 to access the authorization-related variables throughout your Rails application: Rails.configuration.auth0.domain and Rails.configuration.auth0.audience.

Finally, restart your development server to make your Rails project aware of the environment variables:

COMMAND
bin/rails s

Validate a JSON Web Token (JWT) in Rails

You can use the jwt gem to validate JSON Web Tokens in a Rails project. While OAuth 2.0 does not specify a format for access tokens, the Auth0 authorization server issues them in JWT format.

JWT stands for JSON Web Token, an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

Add the gem to your Gemfile by running the following command:

COMMAND
bundle add jwt

Next, create a lib directory under the app directory to store any files related to JSON Web Token (JWT) validation:

COMMAND
mkdir app/lib

Next, create an auth0_client.rb file under the app/lib directory to define an Auth0Client class that encapsulates all the logic required to perform JWT validation in Rails:

COMMAND
touch app/lib/auth0_client.rb
Rails autoloads by default any subdirectories under the app directory. As such, it's more convenient to create the Auth0Client class under an app/lib directory rather than in the lib directory from Rails. If you're not familiar with Rails 6 autoloading with the Zeitwork engine, the Rails guide to autoloading or this article on understanding Zeitwork in Rails 6 can provide more information.

Populate the app/lib/auth0_client.rb file with the following code:

app/lib/auth0_client.rb
# frozen_string_literal: true
require 'jwt'
require 'net/http'
class Auth0Client
# Auth0 Client Objects
Error = Struct.new(:message, :status)
Response = Struct.new(:decoded_token, :error)
# Helper Functions
def self.domain_url
"https://#{Rails.configuration.auth0.domain}/"
end
def self.decode_token(token, jwks_hash)
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: Rails.configuration.auth0.audience.to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})
end
def self.get_jwks
jwks_uri = URI("#{domain_url}.well-known/jwks.json")
Net::HTTP.get_response jwks_uri
end
# Token Validation
def self.validate_token(token)
jwks_response = get_jwks
unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
return Response.new(nil, error)
end
jwks_hash = JSON.parse(jwks_response.body).deep_symbolize_keys
decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)
rescue JWT::VerificationError, JWT::DecodeError => e
error = Error.new('Bad credentials', :unauthorized)
Response.new(nil, error)
end
end
What is the Auth0Client class doing under the hood?

First, the Auth0Client class uses a Ruby Struct to define two objects: Response and Error.

The Response Struct contains the result of the token validation process: if successful, the :decoded_token symbol contains the value of the decoded access token and the :error symbol is nil. Otherwise, if the validation fails, :decoded_token is nil, and :error contains details about the validation error.

The following is an example of what a decoded access token in JWT format looks like:

{
"iss"=>"https://<AUTH0_DOMAIN>/",
"sub"=>"cD7244U03iVauHhfbej7iaf3iQfsAvIdk8@@clients",
"aud"=>"<AUTH0_AUDIENCE>",
"iat"=>1666793694,
"exp"=>1666880094,
"azp"=>"cD700U0feuH1IIgfhj5344QfsAvIdk8",
"gty"=>"client-credentials",
"permissions"=>[]
}
{
"alg"=>"RS256",
"typ"=>"JWT",
"kid"=>"MJjGiAB5k6FUrMf12YUz9"
}

You use the Error Struct to make the error handling process within the Auth0Client class easier. Error wraps the information about the token validation error: it contains a description of what happened, the :message symbol, and the HTTP status code for the client response, the :status symbol.

You can see how the rescue operation uses Error as the second argument of Response.new when the validation fails.

The Auth0Client class defines the following attributes and methods:

The get_jwks method makes an HTTP request to Auth0's well-known endpoint to retrieve the JSON Web Key Set (JWKS), which you must use to validate any of the access tokens that Auth0 issues for your tenant.

The domain_url attribute represents the Auth0 Issuer URL.

The decode_token method decodes the JSON Web Token (JWT). This method receives two arguments: token and jwks_hash.

  • The token argument is the JWT that you want to decode.
  • The jwks_hash argument is a hash that represents a JSON Web Key Sets (JWKS).
The JWKS is a set of keys containing the public keys that you use to verify any JSON Web Token (JWT) that the authorization server (in this case, Auth0) has issued and signed using the RS256 signing algorithm.

The jwks_hash argument has the following format:

{
:keys=>[
{
:alg=>"RS256",
:kty=>"RSA",
:use=>"sig",
:n=>"...",
:e=>"...",
:kid=>"...",
:x5t=>"...,
:x5c=>["..."]
},
...
]
}

The decode_token method calls JWT.decode, a function from the JWT gem with the following arguments:

JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: Rails.configuration.auth0.audience.to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})

What do these arguments represent?

  • The first argument is token, which is the JWT that you want to decode.
  • The second argument refers to the key, for which you use nil as you don't know the key-value pair used to encode the token.
    • If you were using an RSA algorithm, you would encode the token using the private key and decode it using the public key.
  • The third argument is a boolean, verify.
    • If true, JWT.decode will verify the token's signature.
  • The fourth and last argument is a hash with additional configuration options:
{
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: Rails.configuration.auth0.audience.to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys]
}

Let's explore what these JWT.decode options do:

  • The algorithm property represents the signing algorithm that the token-issuer uses to sign the token.
  • The iss property refers to the issuer URL.
  • The verify_iss property is a boolean. If true, JWT.decode checks that the configured issuer matches the JWT issuer present on the JWT payload.
  • The aud property is the audience.
    • In this case, your AUTH0_AUDIENCE variable.
  • The verify_aud property is a boolean. If true, JWT.decode checks that the configured audience matches the audience value present in the JWT payload.
  • The jwks property refers to the JWKS you received from the get_jwks call, which you are storing in jwks_hash.
    • In this case, you only need the keys value.

Finally, the Auth0Client class defines a validate_token(token) method that uses the private class attributes and methods to carry out JWT validation.

The validate_token method receives a JSON Web Token (JWT) as an argument and returns the decoded token after it performs the required validations.

First, validate_token retrieves the JWT public keys using the get_jwks method. If the request is not successful, you return an error response with an appropriate message:

jwks_response = get_jwks
unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: "Unable to verify credentials", status: :internal_server_error)
return Response.new(nil, error)
end

If the request is successful, you parse the json response body into a hash:

jwks_hash = JSON.parse(jwks_response.body).deep_symbolize_keys

You then use the hash to decode the token using the decode_token method and store the result in a Response struct:

decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)

Finally, you catch any validation errors that can happen and return a proper response:

rescue JWT::VerificationError, JWT::DecodeError => e
error = Error.new('Bad credentials', :unauthorized)
Response.new(nil, error)
end

Protect API Endpoints

Create a secured.rb file under the app/controllers/concerns directory to define a Concern that would be used later on the BaseController to validate the supplied access token.

A Rails Concern is a Ruby module that is mixed in between models and controllers, making them available for you to use in any of them. Concerns allow you to extract the common logic from different classes into a reusable module. In this case, you are implementing a Concern so it can be used across other controllers, although for now, you'll only use it in the Auth0Controller.

To create the concern, run the following in your terminal:

COMMAND
touch app/controllers/concerns/secured.rb

Populate the app/controllers/concerns/secured.rb file with the following content:

app/controllers/concerns/secured.rb
# frozen_string_literal: true
module Secured
extend ActiveSupport::Concern
REQUIRES_AUTHENTICATION = { message: 'Requires authentication' }.freeze
BAD_CREDENTIALS = {
message: 'Bad credentials'
}.freeze
MALFORMED_AUTHORIZATION_HEADER = {
error: 'invalid_request',
error_description: 'Authorization header value must follow this format: Bearer access-token',
message: 'Bad credentials'
}.freeze
def authorize
token = token_from_request
return if performed?
validation_response = Auth0Client.validate_token(token)
return unless (error = validation_response.error)
render json: {message: error.message}, status: error.status
end
private
def token_from_request
authorization_header_elements = request.headers['Authorization']&.split
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
unless authorization_header_elements.length == 2
render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return
end
scheme, token = authorization_header_elements
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
token
end
end
How is the access token being validated in the Secured Concern?

There are three validation steps performed on the token_from request method to validate that the access token is well-formed:

  1. Validate that the authorization header is present on the request:
authorization_header_elements = request.headers['Authorization']&.split
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
  1. Validate the authorization header is correctly formed:
unless authorization_header_elements.length == 2
render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return
end
  1. Validate the Bearer scheme is used on the authorization header:
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'

If all those validations are successful, the access token is returned.

The purpose of the authorize method is to call the validate_token method from the Auth0Client class with the provided access token and render any error that may have occurred.

First, you'll need to check if render was already called from the token_from_request method and do nothing if that's the case.

return if performed?

Rails already know the current environment it is working with and will get the proper values. This works great because you may need to set different values for each environment, or set additional variables without changing the Auth0Client class each time you modify it:

validation_response = Auth0Client.new(Rails.configuration.auth0).validate_token(token)

A Concern acts like a mixin, so you'll need to add it to the BaseController, which is the parent controller of all the API controllers, so you don't have to add the same concern to every single controller you'll create. So, let's open the app/controllers/api/base_controller.rb file and add the Secured concern:

app/controllers/api/base_controller.rb
# frozen_string_literal: true
module Api
class BaseController < ApplicationController
include Secured
rescue_from StandardError do |e|
render json: { message: e.message }, status: :internal_server_error
end
end
end

Now, you need to tell Rails to call the authorize method for each endpoint except the public one on the app/controllers/api/messages_controller.rb file. You can do it by registering it as a callback to the before_action built-in method:

app/controllers/api/messages_controller.rb
# frozen_string_literal: true
module Api
class MessagesController < BaseController
before_action :authorize, except: [:public]
def admin
render json: Message.admin_message
end
def protected
render json: Message.protected_message
end
def public
render json: Message.public_message
end
end
end

The authorize method will be called before performing any action on the protected and admin endpoints.

Test Your Protected API Endpoints

If you are using this API with any of the compatible Hello World client applications, you can skip to the "Request Rails API Resources From a Client App" section. Your client application will make authenticated requests to your API.

Run the API server with the following command:

COMMAND
bin/rails s

You can use a terminal application like curl to test that your API server is working as expected.

As an anonymous user:

COMMAND
curl localhost:6060/api/messages/public

You should get a 200 Ok status code with the following message:

{
"text": "This is a public message."
}

Now, let's test the response of a protected endpoint:

COMMAND
curl localhost:6060/api/messages/protected

You should get a 401 Unauthorized status code with the following message:

{
"message": "Requires authentication"
}

As an authenticated user:

An authenticated request is a request that includes a bearer token in its authorization header. That bearer token is the access token in JSON Web Token (JWT) format that you obtained earlier from the Auth0 Dashboard.

Let's test the response of a protected endpoint when using a valid access token:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer AUTH0-ACCESS-TOKEN'

You should get a 200 OK status code with the following message if your access token is valid:

{
"text": "This is a protected message."
}

However, when using an invalid test access token as below:

COMMAND
curl --request GET \
--url http:/localhost:6060/api/messages/protected \
--header 'authorization: Bearer invalidtoken1234567890'

You should get a 401 Unauthorized status code with the following message:

{
"message": "Bad credentials"
}

Set Up Role-Based Access Control (RBAC)

Within the context of Auth0, Role-based access control (RBAC) systems assign permissions to users based on their role within an organization. Everyone who holds that role has the same set of access rights. Those who hold different roles have different access rights.

Developers who use Role-based access control (RBAC) for access management can mitigate the errors that come from assigning permissions to users individually.

You can use the Auth0 Dashboard to enable Role-Based Access Control (RBAC) in any API that you have already registered with Auth0. You then implement RBAC by creating API permissions, assigning those permissions to a role, and assigning that role to any of your users.

Whenever a user logs in to one of your client applications, the Auth0 authorization server issues an access token that the client can use to make authenticated requests to an API server. Auth0 authorization servers issue access tokens in JSON Web Token (JWT) format.

When you enable Auth0 Role-Based Access Control (RBAC) for an API, the access token will include a permissions claim that has all the permissions associated with any roles that you have assigned to that user.

For this particular API, the access token present in the authorization header of a request must include a permissions claim that contains the read:admin-messages permission to access the GET /api/messages/admin endpoint.

Enable Role-Based Access Control (RBAC)

  • Open the APIs section of the Auth0 Dashboard and select your "Auth0 Rails Code Sample" registration.

  • Click on the "Settings" tab and locate the "RBAC Settings" section.

  • Switch on the "Enable RBAC" and "Add Permissions in the Access Token" options.

Visit the "Role-Based Access Control" document for more details.

Create an API permission

In the same Auth0 API registration page, follow these steps:

  • Click on the "Permissions" tab and fill a field from the "Add a Permission (Scope)" section with the following information:
Permission (Scope)
read:admin-messages
Description
Read admin messages
  • Click the "+ Add" button to store the permission.
Visit the "Add API Permissions" document for more details.

Create a role with permissions

Create a role

  • Open the User Management > Roles section of the Auth0 Dashboard.

  • Click on the Create role button and fill out the "New Role" form with the following values:

Name
messages-admin
Description
Access admin messaging features
  • Click on the Create button.
Visit the "Create Roles" document for more details.

Add permissions to the role

  • Click on the "Permissions" tab of the roles page.

  • Click on the "Add Permissions" button.

  • Select the "Auth0 Rails Code Sample" from the dropdown menu that comes up and click the "Add Permissions" button.

  • Select all the permissions available by clicking on them one by one or by using the "All" link.

  • Finally, click on the "Add Permissions" button to finish up.

Visit the "Add Permissions to Roles" document for more details.

Create an admin user

  • Open the User Management > Users section from the Auth0 Dashboard.

  • Click on the "Create user" button and fill out the form with the required information. Alternatively, you can also click on any of your existing users to give one of them the admin role.

  • On the user's page, click on the "Roles" tab and then click on the "Assign Roles" button.

  • Select the messages-admin role from the dropdown menu and click on the "Assign" button.

Visit the "Assign Roles to Users" document for more details.

Implement RBAC in a Ruby on Rails API

Implementing RBAC consists of checking that the access token has the required permissions. Since you already have most of the logic in place to validate an access token, you'll just need to add a method to validate if the permissions claim of the access token contains the required permission to access an endpoint.

Let's implement a method that will validate the access token's permission.

So let's open the app/lib/auth0_client.rb and add the following code:

app/lib/auth0_client.rb
# frozen_string_literal: true
require 'jwt'
require 'net/http'
class Auth0Client
# Class members
Response = Struct.new(:decoded_token, :error)
Error = Struct.new(:message, :status)
Token = Struct.new(:token) do
def validate_permissions(permissions)
required_permissions = Set.new permissions
token_permissions = Set.new token[0]['permissions']
required_permissions <= token_permissions
end
end
# Helper Functions
def self.domain_url
"https://#{Rails.configuration.auth0.domain}/"
end
def self.decode_token(token, jwks_hash)
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: Rails.configuration.auth0.audience.to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})
end
def self.get_jwks
jwks_uri = URI("#{domain_url}.well-known/jwks.json")
Net::HTTP.get_response jwks_uri
end
# Token Validation
def self.validate_token(token)
jwks_response = get_jwks
unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
return Response.new(nil, error)
end
jwks_hash = JSON.parse(jwks_response.body).deep_symbolize_keys
decoded_token = decode_token(token, jwks_hash)
Response.new(Token.new(decoded_token), nil)
rescue JWT::VerificationError, JWT::DecodeError => e
error = Error.new('Bad credentials', :unauthorized)
Response.new(nil, error)
end
end

This code converts the required_permissions and token_permissions variables into Set so that we can easily compare them with the subset operator, <=. An access token will have valid permissions if required_permissions is a subset of token_permissions. Remember that the decoded token from JWT.decode returns an array like [payload, header], so you'll need to get the permissions key from the payload hash. You also have changed the response to include the new Token struct.

Now that the response contains the Token struct, you can validate its permissions. Open the app/controllers/concerns/secured.rb file and add the following method to the Secured module where you previously wrote the token validation:

app/controllers/concerns/secured.rb
# frozen_string_literal: true
module Secured
extend ActiveSupport::Concern
REQUIRES_AUTHENTICATION = { message: 'Requires authentication' }.freeze
BAD_CREDENTIALS = {
message: 'Bad credentials'
}.freeze
MALFORMED_AUTHORIZATION_HEADER = {
error: 'invalid_request',
error_description: 'Authorization header value must follow this format: Bearer access-token',
message: 'Bad credentials'
}.freeze
INSUFFICIENT_PERMISSIONS = {
error: 'insufficient_permissions',
error_description: 'The access token does not contain the required permissions',
message: 'Permission denied'
}.freeze
def authorize
token = token_from_request
return if performed?
validation_response = Auth0Client.validate_token(token)
@decoded_token = validation_response.decoded_token
return unless (error = validation_response.error)
render json: { message: error.message }, status: error.status
end
def validate_permissions(permissions)
raise 'validate_permissions needs to be called with a block' unless block_given?
return yield if @decoded_token.validate_permissions(permissions)
render json: INSUFFICIENT_PERMISSIONS, status: :forbidden
end
private
def token_from_request
authorization_header_elements = request.headers['Authorization']&.split
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
unless authorization_header_elements.length == 2
render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return
end
scheme, token = authorization_header_elements
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
token
end
end

First, you'll check the validate_permissions method was called with a block, such as:

validate_permissions(array) do
# block to run if validation is successful
end

Then, if the permissions from the access token match the permissions argument, the given block is processed on the yield keyword. Otherwise, the Rails API will respond with a 403 Forbidden status code and a proper message response.

Finally, open the app/controllers/api/messages_controller.rb file and modify the admin action to look like this:

app/controllers/api/messages_controller.rb
# frozen_string_literal: true
module Api
class MessagesController < BaseController
before_action :authorize, except: [:public]
def admin
validate_permissions ['read:admin-messages'] do
render json: Message.admin_message
end
end
def protected
render json: Message.protected_message
end
def public
render json: Message.public_message
end
end
end

The required permissions consist of an array of strings where each string is a permission, such as: ['read:admin-messages']. This allows you to define more permissions in the future without changing the rest of the code. Then you'll pass the block you want to be executed when the validation is successful.

Request Rails API Resources From a Client App

Let's test access to your API endpoints by simulating a real user login and requesting protected resources using a real access token.

You can pair this API server with a client application that matches the technology stack that you use at work. Any "Hello World" client application can communicate with this API server.

Pick a Single-Page Application (SPA) code sample in your preferred frontend framework and language:

angular
typescript
Angular Standalone Components Code Sample:Basic Authentication
This code sample uses Angular Standalone Components with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
angular
typescript
Angular Code Sample:Basic Authentication
This code sample uses Angular with TypeScript to implement single-page application authentication using the Auth0 Angular SDK.
react
javascript
React Router 6 Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and JavaScript.
react
typescript
React Router 6/TypeScript Code Sample:Basic Authentication
Code sample showing how to protect a simple React single-page application using React Router 6, Auth0, and TypeScript.
react
javascript
React Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a React Single-Page Application (SPA) using Auth0.
react
typescript
React/TypeScript Code Sample:Basic Authentication
Code sample of a simple React single-page application built TypeScript that implements authentication using Auth0.
svelte
javascript
Svelte Code Sample:Basic Authentication
JavaScript code that implements user login, logout and sign-up features to secure a Svelte Single-Page Application (SPA), using routing middleware.
vue
javascript
Vue.js Composition API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Composition API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js Options API Code Sample:Basic Authentication
This code sample uses Vue.js 3 with JavaScript and the Options API to implement single-page application authentication using the Auth0 Vue SDK.
vue
javascript
Vue.js 2 Code Sample:Basic Authentication
This code sample uses Vue.js 2 with JavaScript to implement single-page application authentication using the Auth0 SPA SDK.

Once you set up the client application, log in and visit the guarded "Admin" page (http://localhost:4040/admin).

When you log in to a "Hello World" client application as a user who has the messages-admin role, your access token will have the required permissions to access the GET /api/messages/admin endpoint, and you'll get the following response:

{
"text": "This is an admin message."
}

However, when you log in as a user who doesn't have the messages-admin role, you'll get a 403 Forbidden status code and the following response:

{
"message": "Permission denied"
}

Conclusion

You have implemented user authorization in Ruby on Rails to identify your users and control the content your users can access by protecting routes and API resources.

This guide covered the most common authorization use cases for a Ruby on Rails API application: use concerns to enforce security policies, validate access tokens, make authenticated requests to your API, and implement Role-Based Access Control (RBAC). 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 authorization strategies in future guides, such as Fine-Grained Authorization (FGA).