> For the complete documentation index, see [llms.txt](https://fusionauth.io/docs/llms.txt)

# FusionAuth QuickStart for Ruby on Rails API | FusionAuth Docs

QuickStart Integration of a Ruby on Rails API with FusionAuth.

# FusionAuth QuickStart for Ruby on Rails API

[Edit on GitHub](https://github.com/FusionAuth/fusionauth-site/blob/main/astro/src/content/docs/get-started/quickstarts/api/quickstart-ruby-on-rails-api.mdx)

[View Markdown](https://fusionauth.io/docs/get-started/quickstarts/api/quickstart-ruby-on-rails-api.md)

In this QuickStart, you are going to learn how to integrate a Ruby on Rails resource server with FusionAuth. You will protect an API resource from unauthorized usage. You'll be building it for [ChangeBank](https://www.youtube.com/watch?v=CXDxNCzUspM), a global leader in converting dollars into coins.

The Docker Compose file and source code for a complete application are available at [https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-api](https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-api).

## Prerequisites

*   [Ruby 2.7.x](https://rubyonrails.org/): This QuickStart was built using Ruby 2.7. This example may work on different versions of Rails, but it has not been tested.
*   [Rails 7.0.x.x](https://rubyonrails.org/): This QuickStart was built using Rails 7.0.7.2. This example may work on different versions of Rails, but it has not been tested.
*   [SQLite](https://www.sqlite.org/download.html): This QuickStart was built using sqlite3 (please note MacOS comes with sqlite3).
*   [Docker](https://www.docker.com): The quickest way to stand up FusionAuth. Ensure you also have [docker compose](https://docs.docker.com/compose/) installed.
*   (Alternatively, you can [Install FusionAuth Manually](https://fusionauth.io/docs/get-started/download-and-install.md)).

## General Architecture

A client wants access to an API resource at `/resource`. However, it is denied this resource until it acquires an access token from FusionAuth.

```mermaid
    sequenceDiagram
        participant Client
        participant App as Resource Server
        participant FusionAuth

        Client ->> App : GET /resource
        App ->> Client : 401 Not Authorized
        Client ->> FusionAuth : POST /api/login
        FusionAuth ->> Client : 200 Ok<br/>(token)
        Client ->> App : GET /resource
        App ->> Client : 200 Ok<br/>(resource) 
```

Resource Server Authentication with FusionAuth

While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the [OAuth grants](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md).

## Getting Started

In this section, you'll get FusionAuth up and running and create a resource server which will serve the API.

### Clone The Code

First off, grab the code from the repository and change into that directory.

```plaintext
git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-api
cd fusionauth-quickstart-ruby-on-rails-api
```

### Run FusionAuth Via Docker

You'll find a Docker Compose file (`docker-compose.yml`) and an environment variables configuration file (`.env`) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

```shell-session
docker compose up -d
```

Here you are using a bootstrapping feature of FusionAuth called [Kickstart](https://fusionauth.io/docs/get-started/download-and-install/development/kickstart.md). When FusionAuth comes up for the first time, it will look at the `kickstart/kickstart.json` file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing `docker compose down -v`, then re-run `docker compose up -d`.

FusionAuth will be initially configured with these settings:

*   Your client Id is `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e`.
*   Your client secret is `super-secret-secret-that-should-be-regenerated-for-production`.
*   Your example teller username is `teller@example.com` and the password is `password`. They will have the role of `teller`.
*   Your example customer username is `customer@example.com` and the password is `password`. They will have the role of `customer`.
*   Your admin username is `admin@example.com` and the password is `password`.
*   The base URL of FusionAuth is `http://localhost:9011/`.

You can log in to the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the [FusionAuth app](http://localhost:9011/admin). The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the `View` icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the `OAuth configuration` section.

The `.env` file contains passwords. In a real application, always add this file to your `.gitignore` file and never commit secrets to version control.

## Create Your Ruby on Rails Resource Server Application

Now you are going to create a Ruby on Rails API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.

We are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints:

*   `make-change`: This endpoint will allow you to call GET with a `total` amount and receive a response indicating how many nickels and pennies are needed to make change. Valid roles are `customer` and `teller`.
*   `panic`: Tellers may call this endpoint to call the police in case of an incident. The only valid role is `teller`.

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in the `Authorization` header in order to be accessed. Additionally, the JWT must have a `roles` claim containing the appropriate role to use the endpoint.

If you simply want to run the application , there is a completed version in the 'complete-application' directory. You can use the following commands to get it up and running if you do not want to create your own.

```shell
cd complete-application
bundle install
bundle e rails s -p 4001
```

You can then follow the instructions in the [Run the API](#run-the-api) section started at Get a Token.

### Initialize The Application

Initialize the Ruby on Rails application using the following:

```plaintext
rails new your-application --api
cd your-application
```

Once this is complete, you will see a new directory called `your-application` with several sub directories.

You need to add the following lines to the `Gemfile` in the application root directory to include two new dependencies:

```shell
gem 'rack-jwt', git: 'https://github.com/FusionAuth/rack-jwt'
gem 'dotenv-rails'
```

Your full `Gemfile` will now look like

```text
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "2.7.5"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.8"

# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 5.0"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"

# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem "rack-cors"

gem 'rack-jwt', git: 'https://github.com/FusionAuth/rack-jwt'

gem 'dotenv-rails'

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri mingw x64_mingw ]
end

group :development do
  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
  # gem "spring"
end
```

Then, install these new gems, by issuing the following command in your terminal window.

```shell
bundle install
```

### Update Routes Config

Update `/config/routes.rb` to include the two new routes that will be created for `/make-change` and `/panic`. Your `routes.rb` file should now match the below code.

```ruby
Rails.application.routes.draw do
  resources :make_change, :path => '/make-change'
  resources :panic, :path => '/panic'
end
```

### Add Security

Create a new file to hold your environment variables directly in the `your-application` directory called `.env.development`. You will need to add two variables that are used to call your FusionAuth instance `FUSIONAUTH_LOCATION` and `CLIENT_ID`, with the values that match below.

```plaintext
# for rails

FUSIONAUTH_LOCATION=http://localhost:9011

CLIENT_ID=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
```

Utilizing the environment variables you just added, you can now setup JSON Web Token (JWT) based authentication. Create a new file `config/initializers/jwt_rack.rb`. This initializer is used to hold configuration settings that are made after all of the frameworks and plugins are loaded.

```ruby
require 'net/http'
require 'jwt'

source = ENV['FUSIONAUTH_LOCATION'] + '/.well-known/jwks.json'
resp = Net::HTTP.get_response(URI.parse(source))
data = resp.body
jwks_hash = JSON.parse(data)
jwks = JWT::JWK::Set.new(jwks_hash)
jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only

p source

jwt_auth_args = {
      secret: nil,
      options: {
        cookie_name: 'app.at',
        iss: ENV['FUSIONAUTH_LOCATION'],
        verify_iss: true,
        aud: ENV['CLIENT_ID'],
        verify_aud: true,
        verify_iat: true,
        verify_expiration: true,
        required_claims: ['applicationId'],
        jwks: jwks,
        algorithm: 'RS256'
      }
}

Rails.application.config.middleware.use Rack::JWT::Auth, jwt_auth_args
```

Having this code protects your endpoints from anonymous users and passes the JWT payload to the controller. The JWT payload includes `roles` encoded in the JWT you receive from FusionAuth. The decoded payload of a JWT for a `teller` might look like this:

```json
{
  "aud": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "exp": 1689289585,
  "iat": 1689285985,
  "iss": "http://localhost:9011",
  "sub": "00000000-0000-0000-0000-111111111111",
  "jti": "ebaa4184-2320-47dd-925b-2e18756c635f",
  "authenticationType": "PASSWORD",
  "email": "teller@example.com",
  "email_verified": true,
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "roles": [
    "teller"
  ],
  "auth_time": 1689285985,
  "tid": "d7d09513-a3f5-401c-9685-34ab6c552453"
}
```

### Add Controllers

Create a new file for the MakeChangeController `app/controllers/make_change_controller.rb`. This controller will verify that the authenticated user has either the `teller` or `customer` role. It then takes in the URL parameter `total` to calculate which coins will be returned in the JSON payload.

```ruby
class MakeChangeController < ApplicationController
    def index
      # further claims/authorization checks
      roles = []
      if request.env['jwt.payload'] && request.env['jwt.payload']['roles']
        roles = request.env['jwt.payload']['roles']
      end
      if (roles & ['teller', 'customer']).empty?
        render json: { message: "You must be a teller or customer for this action." }.to_json, status: :unauthorized
        return
      end
    
      total = params[:total]
      message = "We can make change using"
      begin
      remainingAmount = total.to_f
      rescue message = "Problem converting the submitted value to a decimal.  Value submitted: "+total
      end

      coins = {
        0.25 => "quarters",
        0.10 => "dimes",
        0.05 => "nickels",
        0.01 => "pennies"
      }

      output = {
        Message: message,
        Change: []
      }

      coins.each do |value, coinName|
        coinCount = (remainingAmount / value.to_f).to_i
        remainingAmount = ((remainingAmount - coinCount * value) * 100).round.to_f / 100
        output[:Message] += " " + coinCount.to_s + " " + coinName
        output[:Change].push({Denomination: coinName, Count: coinCount})
      end

      render json: output.to_json, status: :ok
    end
  end
```

Create a new file for the PanicController `app/controllers/panic_controller.rb`. This controller will verify that the authenticated user has only the `teller` role. It will respond with `Proper role not found for user.` if this role is not found. This route also verifies that this is a `POST` request and if this is not true it will respond with `Only POST method is supported.` If both of these tests are passed the controller will return a successful message of `We've called the police!`.

```ruby
class PanicController < ApplicationController
  def index
      render json: { message: "Only POST method is supported." }.to_json, status: :unauthorized
  end
  def create
    # further claims/authorization checks
    roles = []
    if request.env['jwt.payload'] && request.env['jwt.payload']['roles']
      roles = request.env['jwt.payload']['roles']
    end
    if (roles & ['teller']).empty?
      render json: { message: "Proper role not found for user." }.to_json, status: :unauthorized
      return
    end
    render json: { message: "We've called the police!" }.to_json, status: :ok
  end
end
```

## Run the API

Start the API resource server by running:

In a command shell window, navigate to the root directory of your application and run the following command to start the server.

```shell
bundle e rails s -p 4001
```

### Get a Token

There are [several ways to acquire a token](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md) in FusionAuth, but for this example you will use the [Login API](https://fusionauth.io/docs/apis/login.md) to keep things simple.

You will want to leave the server running in one terminal and open another terminal for the following commands.

First let's try the requests as the `teller@example.com` user. Based on the configuration this user has the `teller` role and should be able to use both `/make-change` and `/panic`.

Acquire an access token for `teller@example.com` by making the following request

```shell
curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "teller@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'
```

Your response should look like the below JSON. Grab the token field (which begins with ey).

```json
{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
    "tokenExpirationInstant": 1689353059670,
    "user": {
        ...
    }
}
```

### Make the Request

The code is set up to extract the token from either a cookie or the Authorization header so depending on your preference you can replace `--cookie 'app.at=<your_token>'` with `--header 'Authorization: Bearer <your_token>'` when making requests to the API.

Make a request to `/make-change` with a query parameter `total=1.02`. Use the `token` as the `app.at` cookie.

```shell
curl 'http://localhost:4001/make-change?total=1.02' \
--cookie 'app.at=<your_token>'
```

Alternatively you can make the same request by passing your token in the `Authorization` header.

```shell
curl --location 'http://localhost:4001/make-change?total=1.02' \
--header 'Authorization: Bearer <your_token>'
```

Your response should look like this:

```json
{
  "Message": "We can make change using 20 quarters 1 dimes 0 nickels 2 pennies",
  "Change": [
    {
      "Denomination": "quarters",
      "Count": 20
    },
    {
      "Denomination": "dimes",
      "Count": 1
    },
    {
      "Denomination": "nickels",
      "Count": 0
    },
    {
      "Denomination": "pennies",
      "Count": 2
    }
  ]
}
```

You were authorized, success! You can try making the request without the `--cookie` or with a different string rather than a valid token, and see that you are denied access.

Next call the `/panic` endpoint because you are in trouble!

```shell
curl --location --request POST 'http://localhost:4001/panic' \
--cookie 'app.at=<your_token>'
```

This is a `POST` not a `GET` because you want all your emergency calls to be non-idempotent.

Your response should look like this:

```json
{"message":"We've called the police!"}
```

Nice, help is on the way!

Now let's try as `customer@example.com` who has the role `customer`. Acquire a token for `customer@example.com`.

```shell
curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "customer@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'
```

Your response should look like the below JSON. Grab the token field (which begins with ey).

```json
{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTQxMjMsImlhdCI6MTY4OTM1MzUyMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMjIyMjIyMjIyMjIyIiwianRpIjoiYjc2YWMwMGMtMDdmNi00NzkzLTgzMjgtODM4M2M3MGU4MWUzIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6ImN1c3RvbWVyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJyb2xlcyI6WyJjdXN0b21lciJdLCJhdXRoX3RpbWUiOjE2ODkzNTM1MjMsInRpZCI6ImQ3ZDA5NTEzLWEzZjUtNDAxYy05Njg1LTM0YWI2YzU1MjQ1MyJ9.T1bELQ6a_ItOS0_YYpvqhIVknVMNeamcoC7BWnPjg2lgA9XpCmFA2mVnycoeuz-mSOHbp2cCoauP5opxehBR2lCn4Sz0If6PqgJgXKEpxh5pAxCPt91UyfjH8hGDqE3rDh7E4Hqn7mb-dFFwdfX7CMdKvC3dppMbXAGCZTl0LizApw5KIG9Wmt670339pSf5lzD38P9WAG5Wr7fAmVrIJPVu6yv2FoR-pMYD2lnAF63HWKknrWB-khmhr9ZKRLXKhP1UK-ThY1FSnmpp8eNblsBqCxf6WaYxYkdp5_F2e56M4sQwHzrg4P9tZGVCmMri4dShF3Ck7OGa7hel-iIPew",
    "tokenExpirationInstant": 1689354123118,
    "user": {
        ...
    }
}
```

Now use that token to call `/make-change` with a query parameter `total=3.24`

```shell
curl --location 'http://localhost:4001/make-change?total=3.24' \
--cookie 'app.at=<your_token>'
```

Your response should look like this:

```json
{
    "Message": "We can make change using 12 quarters 2 dimes 0 nickels 4 pennies",
    "Change": [
        {
            "Denomination": "quarters",
            "Count": 12
        },
        {
            "Denomination": "dimes",
            "Count": 2
        },
        {
            "Denomination": "nickels",
            "Count": 0
        },
        {
            "Denomination": "pennies",
            "Count": 4
        }
    ]
}
```

So far so good. Now let's try to call the `/panic` endpoint. (We're adding the `-i` flag to see the headers of the response)

```shell
curl -i --request POST 'http://localhost:4001/panic' \
--cookie 'app.at=<your_token>'
```

You will get a `401 Unauthorized` response with the following message.

```json
{"message":"Proper role not found for user"}
```

Uh oh, I guess you are not allowed to do that.

Enjoy your secured resource server!

## Made it this far? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

\*Offer only valid in the United States and Canada, while supplies last.

[!fusionauth tshirt](https://go.fusionauth.io/tshirt)

## Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you're going to want to do.

### FusionAuth Integration

*   Rather than call the Login API, you're probably going to want to use the [Authorization Code grant](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md#example-authorization-code-grant), which keeps all sensitive credentials within the bounds of FusionAuth. You can customize the [hosted login pages](https://fusionauth.io/docs/customize/look-and-feel.md).
*   You may want to generate a token using the [Client Credentials grant](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md#example-client-credentials-grant) if you are calling the API from another service.

### Security

*   Customize the [token expiration times and policies](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md#configure-application-oauth-settings) in FusionAuth
*   [Make sure you know how to securely consume a token](https://fusionauth.io/articles/tokens/building-a-secure-jwt.md#consuming-a-jwt)
*   Secure your API [using an API gateway](https://fusionauth.io/docs/extend/examples/api-gateways.md) rather than at the framework layer.

## Troubleshooting

*   I get `This site can't be reached localhost refused to connect.` when I call the Login API.

Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, `admin@example.com` with a password of `password` at [http://localhost:9011/admin](http://localhost:9011/admin).

*   The `/panic` endpoint doesn't work when I call it.

Make sure you are making a `POST` call and using a token with the `teller` role.

*   It still doesn't work

You can always pull down a complete running application and compare what's different.

```plaintext
git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-api.git
cd fusionauth-quickstart-ruby-on-rails-api
docker compose up -d
cd complete-application
bundle install
bundle e rails s -p 4001
```