Sign Up
Hero

Add Authorization to a Sinatra API using Auth0

Learn how to implement a Sinatra API and protect its endpoints using Auth0

Sinatra is one of the most popular Ruby frameworks for developing web applications and APIs, used by over 200K applications. Sinatra is a Domain Specific Language (DSL) for creating web applications and APIs in Ruby; it's not your typical web app Model-View-Controller framework but ties specific URLs directly to relevant Ruby code and returns its output in response.

In this blog post, you'll learn how to build a Sinatra API and protect its endpoints using Auth0. You'll build Sinatra Songs API, a songs CRUD API, and learn about the top songs of Frank Sinatra because there's no Sinatra API without some Frank Sinatra in it! ๐Ÿ˜‰

Project Requirements

For this project, you'll use the following versions:

Try out the most powerful authentication platform for free.Get started โ†’

You'll build a Sinatra Songs API from scratch, but if you need it, you can check out the project repository, which has two branches. The main branch contains the songs CRUD API, and the add-authorization branch includes the code used to connect with Auth0 and protect your endpoints.

Building the Songs API

Let's start by creating a new project. In your terminal, create a new folder called sinatra-auth0-songs-api and make it your current directory.

Installing Sinatra

Let's go ahead and install Sinatra. First, you'll need to create a Gemfile to handle all your dependencies.

Create a new file, Gemfile, in the root of your project and fill it up with the following:

# Gemfile 

# frozen_string_literal: true

source 'https://rubygems.org'

ruby File.read('.ruby-version').strip

gem 'sinatra', '~> 3.0', '>= 3.0.2'
gem 'puma'

You can specify the ruby version in the Gemfile; this is a common practice I personally like because as explained in the Bundler docs: This makes your app fail faster in case you depend on specific features in a Ruby VM. This way, the Ruby VM on your deployment server will match your local one.

To do so, you specify a file called .ruby-version and populate it with the Ruby version you'll use, as follows:

3.1.2

Finally, install the gems by running the following:

bundle install

And just like that, Sinatra ๐ŸŽฉ is installed! You also installed puma as a web server.

Creating the song model

Let's create a class to represent a song. Create a new folder, models, and a new file, song.rb inside the models directory.

Populate the song.rb file with the following code:

# models/song.rb

# frozen_string_literal: true

# Class to represent a Song
class Song
  attr_accessor :id, :name, :url

  def initialize(id, name, url)
    @id = id
    @name = name
    @url = url
  end

  def to_json(*a)
    {
      'id' => id,
      'name' => name,
      'url' => url
    }.to_json(*a)
  end
end

You are defining a Song class with three attributes: id, name, and url. You are also implementing a more specialized version of the to_json method from Ruby, which will act as a serializer when you render a song as JSON in the controller.

Implement CRUD API

So far, you've only worked with Ruby; now it's time to get hands-on with Sinatra.

Create a new file, api.rb from your terminal and add the following content to the api.rb file, which will serve as a skeleton for the API:

# api.rb

# frozen_string_literal: true

require 'sinatra'
require 'json'

before do
  content_type 'application/json'
end

get '/songs' do
  return {todo: :implementation}.to_json
end

get '/songs/:id' do
  return {todo: :implementation}.to_json
end

post '/songs' do
  return {todo: :implementation}.to_json
end

put '/songs/:id' do
  return {todo: :implementation}.to_json
end

delete '/songs/:id' do
  return {todo: :implementation}.to_json
end

Let's break down what's going on in the api.rb file.

First, you are requiring the sinatra and json gems.

require 'sinatra'
require 'json'

Unlike Rails, in Sinatra, you have to load everything yourself. This could be great because it removes all the Rails magic ๐Ÿ”ฎ by forcing you to be explicit with what you are using.

Next, you are defining a before filter:

before do
  content_type 'application/json'
end

As explained in the Sinatra Docs, before filters are evaluated before each request.

In this case, you are setting the Content-Type header to application/json, meaning you are making the client aware that all the responses from this server have JSON format.

Next, you define the routes:

get '/songs' do
  # ...
end
get '/songs/:id' do
  # ...
end
post '/songs' do
  # ...
end
put '/songs/:id' do
  # ...
end
delete '/songs/:id' do
  # ...
end

These routes represent the CRUD you'll be implementing.

  • Create: POST /songs
  • Read: GET /songs
  • Read: GET /songs/:id
  • Update: PUT /songs/:id
  • Delete: DELETE /songs/:id

Well, it's more like CRRUD, but you get the point. ๐Ÿซ 

With your API skeleton in place, you can run the server and test the endpoints.

To run the server from your terminal:

ruby api.rb

Once the server is running, your terminal will look as follows:

โžœ  sinatra-auth0-songs-api git:(main) โœ— ruby api.rb 
== Sinatra (v3.0.2) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.0.0 (ruby 3.1.2-p20) ("Sunflower")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 98050
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop

Now you can access the endpoints on http://localhost:4567. I've created a POSTMAN Collection, so you can test the endpoints yourself. You can also use curl like so:

โžœ curl -v http://localhost:4567/songs 
       
{"todo":"implementation"}%

Populate the API with the songs.json file

To have some data in the API, you can download the songs.json file from the repository accompanying this article, which was populated using data from the LastFM API. The songs.json file contains Frank Sinatra's Top 10 Tracks in a simplified version of what LastFM provides with the following format:

{
  "id": 1,
  "name": "My Way",
  "url": "https://www.last.fm/music/Frank+Sinatra/_/My+Way"
}

Let's implement a helper to read from the songs.json file and load the data once the Sinatra API starts.

Create a new folder, helpers, and a songs_helper.rb inside of it and populate it with the following code:

# helpers/songs_helper.rb

# frozen_string_literal: true

require_relative '../models/song'
require 'json'

# Class to read songs from a JSON file
class SongsHelper
  def self.songs
    filepath = File.join(File.dirname(__FILE__), '../songs.json')
    file = File.read(filepath)
    data = JSON.parse(file)['songs']

    data.map do |song|
      Song.new(song['id'], song['name'], song['url'])
    end
  end
end

The SongsHelper class implements a songs method that reads the songs.json file and maps its content into an array of Song objects.

Next, in your api.rb file, you can call the SongsHelper.songs function to load the songs:

# api.rb 

# frozen_string_literal: true

require 'sinatra'
require 'json'
# ๐Ÿ‘‡ new code 
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs
# ๐Ÿ‘† new code 

# existing code ...

You are importing the helpers/songs_helper file, calling the songs method, and storing it in a songs variable.

Note in a real-world app, you'd have a proper database, and there will be no need to do this step, but to keep this tutorial simple, we won't have a database and will work with the data coming from the songs.json file.

Using the songs variable, you can now manage the GET songs request as follows:

# api.rb 

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

before do
  content_type 'application/json'
end

# ๐Ÿ‘‡ new code 
get '/songs' do
  return songs.to_json
end
# ๐Ÿ‘† new code 

# existing code ...

The GET songs request will now retrieve an array of songs, testing it out with curl:

โžœ curl http://localhost:4567/songs 
[
  {"id":1,"name":"My Way","url":"https://www.last.fm/music/Frank+Sinatra/_/My+Way"},
  {"id":2,"name":"Strangers in the Night","url":"https://www.last.fm/music/Frank+Sinatra/_/Strangers+in+the+Night"},
  {"id":3,"name":"Fly Me to the Moon","url":"https://www.last.fm/music/Frank+Sinatra/_/Fly+Me+to+the+Moon"},
  {"id":4,"name":"That's Life","url":"https://www.last.fm/music/Frank+Sinatra/_/That%27s+Life"},
  {"id":5,"name":"I've Got You Under My Skin","url":"https://www.last.fm/music/Frank+Sinatra/_/I%27ve+Got+You+Under+My+Skin"},
  {"id":6,"name":"Come Fly With Me","url":"https://www.last.fm/music/Frank+Sinatra/_/Come+Fly+With+Me"},
  {"id":7,"name":"The Way You Look Tonight","url":"https://www.last.fm/music/Frank+Sinatra/_/The+Way+You+Look+Tonight"},
  {"id":8,"name":"Fly Me to the Moon (In Other Words)","url":"https://www.last.fm/music/Frank+Sinatra/_/Fly+Me+to+the+Moon+(In+Other+Words)"},
  {"id":9,"name":"Theme from New York, New York","url":"https://www.last.fm/music/Frank+Sinatra/_/Theme+from+New+York,+New+York"},
  {"id":10,"name":"Jingle Bells","url":"https://www.last.fm/music/Frank+Sinatra/_/Jingle+Bells"}  
]%

Let's now implement the song details' route, songs/:id. To do so, let's introduce the concept of helpers and implement a new one.

In your api.rb file, add the following content:

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

# ๐Ÿ‘‡ new code
helpers do
  def id_param
    halt 400, { message: 'Bad Request' }.to_json if params['id'].to_i < 1
    params['id'].to_i
  end
end
# ๐Ÿ‘† new code 

# existing code ...

In Sinatra, helpers refer to a top-level method that defines helper methods to use in route handlers and templates.

In this case, you are defining a helper id_param that checks first if the params hash is defined. The params hash is a hash that Sinatra makes available for you in your route blocks, and it will automatically include relevant data from the request.

In the id_param method, you are showing a Bad Request error if the params['id'] value is not positive. When it is a valid value, you return it and convert it to an integer. You'll use the id_param method for all the routes that require :id in the route, meaning:

  • GET /songs/:id
  • PUT /songs/:id
  • DELETE /songs/:id

Now, going back to the api.rb file, you can implement the song details route by making use of the id_param helper method as follows:

# existing code ... 

get '/songs' do
  return songs.to_json
end

get '/songs/:id' do
  # ๐Ÿ‘‡ new code
  song = songs.find { |s| s.id == id_param }
  halt 404, { message: 'Song Not Found' }.to_json unless song

  return song.to_json
  # ๐Ÿ‘† new code 
end

# existing code ...

You are using Ruby's Enumerable#find method to find the song in the songs array that has the ID sent in the params. If the song was not found, then you'll return a 404 NOT FOUND error. Otherwise, you'll return the song in JSON format.

Let's test it out with curl:

โžœ curl http://localhost:4567/songs/1

{"id":1,"name":"My Way","url":"https://www.last.fm/music/Frank+Sinatra/_/My+Way"}%

Noice. At this point, you have implemented both of the read routes from your Songs API. Time to create, update, and delete.

Let's start by the create route. You can create a new song by providing a name and a url. In curl, that POST request will look as follows:

curl -X POST 'http://localhost:4567/songs' \
     -H 'Content-Type: application/json' \
     -d '{
          "name": "A new song",
          "url": "http://example.com"
        }'

You must pass the name and url in the request's body and declare that they come with the correct JSON format. You'll also need to do this when updating a song. This is a hint for you to implement a helper.

Let's implement a new helper, json_params, that will check the body is indeed in JSON format.

In your api.rb, add the following code:

# api.rb

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

helpers do
  # existing code ...
 
  # ๐Ÿ‘‡ new code 
  def json_params
    request.body.rewind
    @json_params ||= JSON.parse(request.body.read).transform_keys(&:to_sym)
  rescue JSON::ParserError
    halt 400, { message: 'Invalid JSON body' }.to_json
  end
  # ๐Ÿ‘† new code 

  # existing code ...
end

# existing code ...

The json_params method reads from the request.body and parses it using JSON.parse. If there is a JSON::ParseError, meaning the body was not in JSON format, the method will return 400 Bad Request.

You should also validate that the body parameters are only the required ones: name and url. Let's create a new helper that will implement that validation:

# api.rb

# existing code ...

helpers do
  # existing code ...
 
  def json_params
    request.body.rewind
    @json_params ||= JSON.parse(request.body.read).transform_keys(&:to_sym)
  rescue JSON::ParserError
    halt 400, { message: 'Invalid JSON body' }.to_json
  end
  
  # ๐Ÿ‘‡ new code
  def require_params!
    json_params

    attrs = %i[name url]

    halt(400, { message: 'Missing parameters' }.to_json) if (attrs & @json_params.keys).empty?
  end
  # ๐Ÿ‘† new code 

  # existing code ...
end

# existing code ...

The require_params! method will be the main method that you'll use in your routes. Firstly, it calls json_params to initialize the instance variable @json_params and is available in the api.rb context. Then, the require_params! method verifies that @json_params.keys includes any of the values name or url and no other parameter. You can think of it as an attempt of Rails' permit method. Otherwise, it returns 400 Bad Request.

The name and url params are only required when creating and updating a song. You can create a before filter to accomplish this.

In your api.rb, add the following:

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

# ๐Ÿ‘‡ new code 
set :method do |*methods|
  methods = methods.map { |m| m.to_s.upcase }
  condition { methods.include?(request.request_method) }
end
# ๐Ÿ‘† new code

helpers do
  # ... existing code 
end

before do
  content_type 'application/json'
end

# ๐Ÿ‘‡ new code 
before method: %i[post put] do
  require_params!
end
# ๐Ÿ‘† new code

Let's break this down. You added two new things: one is a set and the other is a before filter which you are already familiar with.

The set method takes a setting name and value and creates an attribute on the application.

In this case, you'll use it to identify the HTTP method. The setting name is :method, and it takes an array of symbols as an argument. Then, you use a condition, meaning you want this before filter only to execute when the condition is true, in this case, when the HTTP method is POST or PUT.

In the before filter, you are passing a list of symbols representing the HTTP methods where you want this code to execute, and then you call the require_params!.

Now let's add the code for creating and updating songs in your api.rb.

First, to create a new song, you'll handle the POST /songs request:

# existing code ...

before method: %i[post put] do
  require_params!
end

# existing code ...

# ๐Ÿ‘‡ new code 
post '/songs' do
  create_params = @json_params.slice(:name, :url)

  if create_params.keys.sort == %i[name url]
    new_song = { id: songs.size + 1, name: @json_params[:name], url: @json_params[:url] }
  else
    halt(400, { message: 'Missing parameters' }.to_json)
  end

  songs.push(new_song)

  return new_song.to_json
end
# ๐Ÿ‘†new code 

# existing code ...
end
# existing code ...

The post /songs route tries to keep things simple. First, it verifies that both params name and url are present in the @json_params hash; keep in mind the require_params! filter already made sure these are the only parameters being passed. If the name and url are present, you can create a new song. Note you are just incrementing the songs.size value by 1 and then pushing the new song to the songs array. In a real-life application, you would create a new record in your database. If the name or url parameters are missing, then you return a 400 Bad Request error.

Let's proceed to add the code for the update route, put /songs/:id:

# api.rb 
# existing code ... 

# ๐Ÿ‘‡ new code 
put '/songs/:id' do
  song = songs.find { |s| s.id == id_param }

  halt 404, { message: 'Song Not Found' }.to_json unless song

  song.name = @json_params[:name] if @json_params.keys.include? :name
  song.url = @json_params[:url] if @json_params.keys.include? :url

  return song.to_json
end
# ๐Ÿ‘†new code 

# existing code ...
end

When requested to update a song, your code attempts to find the song in the songs array using the id_param, similar to the song details route. If it's not found, it returns a 404 Not Found error. If the song is found, it updates only the field that was sent in the request body and finally returns the song in JSON format.

Last but not least, there is the delete song route, delete /songs/:id. Let's add it to the api.rb file:

# api.rb 
# existing code ... 

# ๐Ÿ‘‡ new code 
delete '/songs/:id' do
  song = songs.find { |s| s.id == id_param }
  halt 404, { message: 'Song Not Found' }.to_json unless song

  song = songs.delete(song)

  return song.to_json
end
# ๐Ÿ‘†new code 

# existing code ...
end

The delete song method is very similar to the update song method, but instead, it calls the Array#delete function and renders the song in JSON format as well.

Your Songs API is finished! But not secured ๐Ÿ˜ฉ. At this point, your code must be very similar to the one on the main branch of the repository.

Protect Your Endpoints with Auth0

Up until here, you have created a CRUD Songs API, but anyone can call any endpoint. You want to make sure only authorized users can create, update, and delete songs.

To achieve this, you'll use Auth0 as your identity access management (IAM) provider.

Note that from this point on, you will be writing the code that is already implemented in the add-authorization branch, so you can use it as a guide.

Connect your Sinatra API with Auth0

Before you jump into the code, you'll need to create a new Auth0 API. Head to the API section of your Auth0 dashboard and click the "Create API" button. Fill out the form with the following values:

  • Name: Sinatra Songs API
  • Identifier: https://sinatra-auth0-songs-api
  • Signing Algorithm: RS256 (this should be the default selection)

Copy the identifier value (https://sinatra-auth0-songs-api) โ€” you'll need it when setting up your Sinatra API. You'll also need to grab your Auth0 Domain. Unless you're using a custom domain, this value will be [TENANT_NAME].[REGION].auth0.com. If you're unsure what this value is, open the "Test" tab in your API's settings and look at the url argument in the code sample under "Asking Auth0 for tokens from my application":

Once you've finished creating your API, you can head to the command line and start installing the dependencies.

Install dependencies

You'll need a few gems, so let's go ahead and add them to the Gemfile:

gem 'dotenv'
gem 'jwt'

Next, in your terminal, run:

bundle install

You are installing the dotenv gem to read environment variables from a local .env file. You can use the .env.example file from the repository as a template and copy its content to a .env file in the root of your project.

Remember in the previous step; you had to save your Auth0 domain and identifier? Well, this is where you get to use it.

Paste your AUTH0_DOMAIN and AUTH0_IDENTIFIER into your .env file.

You also installed the JWT gem, which is a Ruby implementation of the JWT standard and will help you later on to validate JWT tokens, you'll learn more about those in a bit.

Validate the access token

In order to protect your API's endpoints, you'll use what's called token-based authorization. Basically, your Sinatra Songs API will receive an access token; the passed access token informs the API that the bearer of the token has been authorized to access the API and perform specific actions specified by the scope.

Finally, your API will validate the access token by making sure it has the proper structure and that it was issued by the correct authorization server, in this case, Auth0.

Create an Auth0 Client class

The first step to validate the access token is to create a new class to take care of the process.

In your helpers folder, create a new file called auth0_client_helper.rb and add the following code:

# helpers/auth0_client_helper.rb

# frozen_string_literal: true

require 'jwt'
require 'net/http'

# AuthoClient helper class to validate JWT access token
class Auth0ClientHelper
  # Auth0 Client Objects
  Error = Struct.new(:message, :status)
  Response = Struct.new(:decoded_token, :error)

  # Helper Functions
  def self.domain_url
    "https://#{ENV['AUTH0_DOMAIN']}/"
  end

  def self.decode_token(token, jwks_hash)
    JWT.decode(token, nil, true, {
                 algorithm: 'RS256',
                 iss: domain_url,
                 verify_iss: true,
                 aud: (ENV['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).transform_keys(&:to_sym)

    decoded_token = decode_token(token, jwks_hash)
    Response.new(decoded_token, nil)
  rescue JWT::VerificationError, JWT::DecodeError
    error = Error.new('Bad credentials', 401)
    Response.new(nil, error)
  end
end

There are a few things going on in this class, and I've explained it extensively in the Rails API Authorization By Example Guide, particularly the section Validate a JSON Web Token (JWT) in Rails under "What is the Auth0Client class doing under the hood?". Of course, I made a few changes to modify the code from Rails to Sinatra, but the main idea remains.

You can learn more about these security concepts in practice using the Rails Authentication By Example guide and the Rails Authorization By Example guide, which also covers the concept of Role-Based Access Control (RBAC).

With that being said, let's take a look at the main method in this class: the validate_token method.

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).transform_keys(&:to_sym)

    decoded_token = decode_token(token, jwks_hash)
    Response.new(decoded_token, nil)
  rescue JWT::VerificationError, JWT::DecodeError
    error = Error.new('Bad credentials', 401)
    Response.new(nil, error)
  end

Let's break down what the validate_token method is doing:

  1. First, you call the get_jwks method, which in summary calls Auth0's well-known endpoint and returns the JSON Web Key Set (JWKS) used to verify all Auth0-issued JWTs for your tenant. If there was an error getting the JWKS, then you throw an error because the token could not be validated.
  2. Next, you parse the JWKS into a hash to make it easier to work with in Ruby.
  3. Finally, you call the decode_token method, which uses the JWT gem to decode the access token as follows:

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

The domain_url gets your AUTH0_DOMAIN from the environment variables, and you set your AUTH0_AUDIENCE in the aud value. Finally, you pass the jwks_hash you created earlier in the jwks argument.

To learn more about the JWT.decode arguments, you can refer to the Rails API Authorization By Example Developer Guide, section "What is the Auth0Client class doing under the hood?".

Create an authorize! helper

The Auth0ClientHelper class is already doing most of the work to validate the access token. Now you need to actually call it in the endpoints you want to protect.

For that, you can use a helper, similar to how you used it earlier.

Go to your api.rb file and add the following code:

# api.rb 

# existing code ...

helpers do
  # existing code ... 

  # ๐Ÿ‘‡ new code
  def authorize!
    token = token_from_request

    validation_response = Auth0ClientHelper.validate_token(token)

    return unless (error = validation_response.error)

    halt error.status, { message: error.message }.to_json
  end

  def token_from_request
    authorization_header_elements = request.env['HTTP_AUTHORIZATION']&.split

    halt 401, { message: 'Requires authentication' }.to_json unless authorization_header_elements

    unless authorization_header_elements.length == 2
      halt 401, { message: 'Authorization header value must follow this format: Bearer access-token' }.to_json
    end

    scheme, token = authorization_header_elements

    halt 402, { message: 'Bad credentials' }.to_json unless scheme.downcase == 'bearer'

    token
  end
  # ๐Ÿ‘† new code
end

# existing code ...

Well, you actually added two helpers but the token_from_request method is the helper of the authorize! helper ๐Ÿ˜›.

The authorize! helper gets the token from the request by calling the token_from_request method. This method checks the HTTP_AUTHORIZATION header and splits it to verify it is well-formed.

A well-formed Authorization header using the bearer scheme looks like this:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ

Then, the token_from_request method verifies if the Authorization header is present, if the token is present, and if it has the correct scheme. Otherwise, it will return 401 Unauthorized.

Once the token is retrieved from the Authorization header, the authorize! helper calls the validate_token method of the Auth0ClientHelper class to validate the token. If the token was validated without errors, the authorize! method finished its execution. If there is any error during the validation, it returns it with a proper status and message.

Protect your API endpoints with the authorize! helper

The last step to protect your endpoints is to call the authorize! helper before any client tries to call them.

So as you saw earlier, a before filter is the way to go.

In your api.rb file, you already had a before filter that you can reuse, so let's modify it:

# api.rb 
# existing code ... 

# old code
# before method: %i[post put] do
#   require_params!
# end
# old code

# ๐Ÿ‘‡ new code
before method: %i[post put delete] do
  require_params!
  authorize!
end
# ๐Ÿ‘† new code
# existing code ...

First, you added the delete method to the before filter because you want only authorized users to be able to create, update and delete songs.

Then you call the authorize! helper that will perform the authorization validation.

That's it! You can now test your endpoints with curl as follows:

curl -X POST 'http://localhost:4567/songs' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
    "name": "A new song"
    "url": "http://example.com"
}'

Once you replace the YOUR_TOKEN placeholder with a valid access token, the result of this request will be the following:

{"id":11,"name":"A new song","url":"http://example.com"}

To get a valid access token for your API, follow the steps shown in the section Connect your Sinatra API With Auth0.

Summary

In this blog post, you learned about the Ruby framework Sinatra and how to create a basic CRUD API to manage Frank Sinatra songs.

You created a new Auth0 account and a new API from the dashboard. You used the JWT gem to validate an access token issued by Auth0 and finally secured your API endpoints for creating, updating, and deleting songs by using token-based authorization and the bearer scheme.

I hope you enjoyed this post. Do you use any other Ruby frameworks? Let me know in the comments!

Thanks for reading!