Integrate Your Ruby on Rails API With FusionAuth
Integrate Your Ruby on Rails API With FusionAuth
In this article, you are going to learn how to integrate a Ruby on Rails API with FusionAuth. This presupposes you’ve built an application that is going to retrieve an access token from FusionAuth via one of the OAuth grants. The grant will typically be the Authorization Code grant for users or the Client Credentials grant for programmatic access.
The token provided by FusionAuth can be stored by the client in a number of locations. For server side applications, it can be stored in a database or on the file system. In mobile applications, store them securely as files accessible only to your app. For a browser application like a SPA, use a cookie if possible and server-side sessions if not.
Here’s a typical API request flow before integrating FusionAuth with your Ruby on Rails API.
Here’s the same API request flow when FusionAuth is introduced.
This document will walk through the use case where a Ruby on Rails API validates the token. You can also use an API gateway to verify claims and signatures. For more information on doing that with FusionAuth, visit the API gateway documentation.
Prerequisites
For this tutorial, you’ll need to have Ruby, bundler and Rails installed.
You’ll also need Docker, since that is how you’ll install FusionAuth.
The commands below are for macOS, but are limited to mkdir
and cd
, which have equivalents in Windows and Linux.
Download and Install FusionAuth
First, make a project directory:
mkdir integrate-fusionauth && cd integrate-fusionauth
Then, install FusionAuth:
curl -o docker-compose.yml https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/.env
docker-compose up -d
Create a User and an API Key
Next, log into your FusionAuth instance. You’ll need to set up a user and a password, as well as accept the terms and conditions.
Then, you’re at the FusionAuth admin UI. This lets you configure FusionAuth manually. But for this tutorial, you’re going to create an API key and then you’ll configure FusionAuth using our client library.
Navigate to + button to add a new API Key. Copy the value of the Key field and then save the key.
It might be a value like CY1EUq2oAQrCgE7azl3A2xwG-OEwGPqLryDRBCoz-13IqyFYMn1_Udjt
.
Doing so creates an API key that can be used for any FusionAuth API call. Save that key value off as you’ll be using it later.
Configure FusionAuth
Next, you need to set up FusionAuth. This can be done in different ways, but we’re going to use the Ruby client library. You can use the client library with an IDE of your preference as well.
First, make a directory:
mkdir setup-fusionauth && cd setup-fusionauth
Then, create the required files:
touch Gemfile
Now, copy and paste the following file into Gemfile
.
source 'https://rubygems.org'
gem "fusionauth_client"
Install the gems.
bundle install
Create a file called setup.rb
. Then copy and paste the following code into it.
require 'fusionauth/fusionauth_client'
APPLICATION_ID = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e";
RSA_KEY_ID = "356a6624-b33c-471a-b707-48bbfcfbc593"
# You must supply your API key as an envt var
api_key_name = 'fusionauth_api_key'
api_key = ENV[api_key_name]
unless api_key
puts "please set api key in the '" + api_key_name.to_s + "' environment variable"
exit 1
end
client = FusionAuth::FusionAuthClient.new(api_key, 'http://localhost:9011')
# set the issuer up correctly
client_response = client.retrieve_tenants()
if client_response.was_successful
tenant = client_response.success_response["tenants"][0]
else
puts "couldn't find tenants " + client_response.error_response.to_s
exit 1
end
client_response = client.patch_tenant(tenant["id"], {"tenant": {"issuer":"http://localhost:9011"}})
unless client_response.was_successful
puts "couldn't update tenant "+ client_response.error_response.to_s
exit 1
end
# generate RSA keypair for signing
client_response = client.generate_key(RSA_KEY_ID, {"key": {"algorithm":"RS256", "name":"For RailsExampleApp", "length": 2048}})
unless client_response.was_successful
puts "couldn't create RSA key "+ client_response.error_response.to_s
exit 1
end
# create application
# too much to inline it
application = {}
application["name"] = "RubyExampleAPI"
# configure oauth
application["oauthConfiguration"] = {}
application["oauthConfiguration"]["authorizedRedirectURLs"] = ["http://localhost:3000/auth/my_provider/callback"]
application["oauthConfiguration"]["clientSecret"] = "change-this-in-production-to-be-a-real-secret"
application["roles"] = ["ceo","dev","intern"]
# assign key from above to sign our tokens. This needs to be asymmetric
application["jwtConfiguration"] = {}
application["jwtConfiguration"]["enabled"] = true
application["jwtConfiguration"]["accessTokenKeyId"] = RSA_KEY_ID
application["jwtConfiguration"]["idTokenKeyId"] = RSA_KEY_ID
client_response = client.create_application(APPLICATION_ID, {"application": application})
unless client_response.was_successful
puts "couldn't create application "+ client_response.error_response.to_s
exit 1
end
# register user, there should be only one, so grab the first
client_response = client.search_users_by_query({"search": {"queryString":"*"}})
unless client_response.was_successful
puts "couldn't find users "+ client_response.error_response.to_s
exit 1
end
user = client_response.success_response["users"][0]
# now register the user
client_response = client.register(user["id"], {"registration":{"applicationId":APPLICATION_ID}})
unless client_response.was_successful
puts "couldn't register user "+ client_response.error_response.to_s
exit 1
end
Then, you can run the setup script.
The setup script is designed to run on a newly installed FusionAuth instance with only one user and no tenants other than Default
. To follow this guide on a FusionAuth instance that does not meet these criteria, you may need to modify the above script.
Refer to the Ruby client library documentation for more information.
This will create the FusionAuth configuration for your Ruby on Rails API.
fusionauth_api_key=YOUR_API_KEY_FROM_ABOVE ruby setup.rb
If you are using PowerShell, you will need to set the environment variable in a separate command before executing the script.
$env:fusionauth_api_key='YOUR_API_KEY_FROM_ABOVE'
ruby setup.rb
If you want, you can log into your instance and examine the new API configuration the script created for you. You’d navigate to the tab to do so.
Create Your Ruby on Rails API
Now you are going to create a Ruby on Rails API. While this section builds a simple Ruby on Rails API, you can use the same configuration to build a more complex Ruby on Rails API.
First, create the skeleton of the Ruby on Rails API. Rails has a nice generator to build this out.
rails new myapi --api && cd myapi
Now, update your Gemfile
to look like this:
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.2.2"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.4", ">= 7.0.4.3"
# 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
You may need to modify the version of ruby specified in the Gemfile. As long as Rails 7 is supported, you will be fine.
Then, install these new gems.
bundle install
Next, create a file called .env.development
and insert the following into it.
# for rails
FUSIONAUTH_LOCATION=http://localhost:9011
CLIENT_ID=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
You can now start writing the code for your Rails API. First, let’s create a controller which gives back a JSON message. Create a new file called app/controllers/messages_controller.rb
, then add the following code:
class MessagesController < ApplicationController
def index
messages = []
messages << "Hello"
# further claims/authorization checks
roles = []
if request.env['jwt.payload'] && request.env['jwt.payload']['roles']
roles = request.env['jwt.payload']['roles']
end
if roles.include?('ceo')
messages << "Hiya, boss"
end
render json: { messages: messages }.to_json, status: :ok
end
end
This controller returns a JSON array with messages.
Next, update the config/routes.rb
file to look like this:
Rails.application.routes.draw do
resources :messages, only: [:index]
end
This tells Ruby on Rails to return the content generated by the messages_controller.rb
file when the /messages
path is set up.
You can now start up your server. You should do it in a new terminal window so that you can continue to edit the Ruby on Rails code.
bundle e rails s -p 4001
And visit http://localhost:4001/messages and view the JSON.
Next, let’s configure the token protection for this API. Go back to your previous terminal window and create a config/initializers/jwt_rack.rb
file, and update it to look like this:
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
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
This tells Ruby on Rails to check for various attributes of the token, including the iss
and the aud
. Read the rack_jwt
gem documentation for more.
You can also access the JWT in the controller. Below, the messages controller adds a special message if the user has a certain role.
class MessagesController < ApplicationController
def index
messages = []
messages << "Hello"
# further claims/authorization checks
roles = []
if request.env['jwt.payload'] && request.env['jwt.payload']['roles']
roles = request.env['jwt.payload']['roles']
end
if roles.include?('ceo')
messages << "Hiya, boss"
end
render json: { messages: messages }.to_json, status: :ok
end
end
Now, back to the terminal where your server is running. Stop it (using control-C
) and restart it.
bundle e rails s -p 4001
Visit http://localhost:4001/messages, you’ll get an error:
{"error":"Missing token cookie and Authorization header"}
Your API is protected. Now, let’s get an access token so authorized clients can get the API results.
Testing the API Flow
There are a number of ways to get an access token, as mentioned, but for clarity, let’s use the login API to mimic a client.
Run this command in a terminal:
curl -H 'Authorization: YOUR_API_KEY_FROM_ABOVE' \
-H 'Content-type: application/json' \
-d '{"loginId": "YOUR_EMAIL", "password":"YOUR_PASSWORD","applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"} \
http://localhost:9011/api/login
Replace YOUR_EMAIL
and YOUR_PASSWORD
with the username and password you set up previously.
This request will return something like this:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODUxNDA5ODQsImlhdCI6MTQ4NTEzNzM4NCwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIyOWFjMGMxOC0wYjRhLTQyY2YtODJmYy0wM2Q1NzAzMThhMWQiLCJhcHBsaWNhdGlvbklkIjoiNzkxMDM3MzQtOTdhYi00ZDFhLWFmMzctZTAwNmQwNWQyOTUyIiwicm9sZXMiOltdfQ.Mp0Pcwsz5VECK11Kf2ZZNF_SMKu5CgBeLN9ZOP04kZo",
"user": {
"active": true,
"birthDate": "1976-05-30",
"connectorId": "e3306678-a53a-4964-9040-1c96f36dda72",
"data": {
"displayName": "Johnny Boy",
"favoriteColors": [
"Red",
"Blue"
]
},
"email": "example@fusionauth.io",
"expiry": 1571786483322,
"firstName": "John",
"fullName": "John Doe",
"id": "00000000-0000-0001-0000-000000000000",
"imageUrl": "http://65.media.tumblr.com/tumblr_l7dbl0MHbU1qz50x3o1_500.png",
"lastLoginInstant": 1471786483322,
"lastName": "Doe",
"middleName": "William",
"mobilePhone": "303-555-1234",
"passwordChangeRequired": false,
"passwordLastUpdateInstant": 1471786483322,
"preferredLanguages": [
"en",
"fr"
],
"registrations": [
{
"applicationId": "10000000-0000-0002-0000-000000000001",
"data": {
"displayName": "Johnny",
"favoriteSports": [
"Football",
"Basketball"
]
},
"id": "00000000-0000-0002-0000-000000000000",
"insertInstant": 1446064706250,
"lastLoginInstant": 1456064601291,
"preferredLanguages": [
"en",
"fr"
],
"roles": [
"user",
"community_helper"
],
"username": "johnny123",
"usernameStatus": "ACTIVE"
}
],
"timezone": "America/Denver",
"tenantId": "f24aca2b-ce4a-4dad-951a-c9d690e71415",
"twoFactorEnabled": false,
"usernameStatus": "ACTIVE",
"username": "johnny123",
"verified": true
}
}
Grab the token
field (which begins with ey
). Replace YOUR_TOKEN below with that value, and run this command:
curl --cookie 'app.at=YOUR_TOKEN' http://localhost:4001/messages
Here you are placing the token in a cookie named app.at
. This is for compatibility with the FusionAuth best practices and the hosted backend.
If you want to store it in a different cookie or send it in the header, make sure you modify the rack_jwt
initializer and restart the Ruby on Rails API.
This will result in the JSON below.
{"messages":["Hello"]}
Feedback
How helpful was this page?
See a problem?
File an issue in our docs repo
Have a question or comment to share?
Visit the FusionAuth community forum.