web
Ruby on Rails
In this quickstart you are going to build an application with Ruby on Rails and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.
The docker compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web
Prerequisites
- Ruby: This will be needed for pulling down the various dependencies.
- Rails: This will be used in order to run the Rails server.
- Docker: The quickest way to stand up FusionAuth. (There are other ways).
This app has been tested with Ruby 3.2.2 and Rails 7.0.4.3. This example should work with other compatible versions of Ruby and Rails.
General Architecture
While this sample application doesn’t have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
Getting Started
In this section, you’ll get FusionAuth up and running and use Rails to create a new application.
Clone the Code
First off, grab the code from the repository and change into that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
cd fusionauth-quickstart-ruby-on-rails-web
Run FusionAuth via Docker
In the root directory of the repo you’ll find a Docker compose file (docker-compose.yml) and an environment variables configuration file (.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:
docker compose up -d
Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to a certain initial state.
If you ever want to reset the FusionAuth system, 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 username is
richard@example.com
and the password ispassword
. - Your admin username is
admin@example.com
and the password ispassword
. - The base URL of FusionAuth
http://localhost:9011/
.
You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.
Create your Ruby on Rails Application
Now you are going to create a Ruby on Rails application. While this section builds a simple Ruby on Rails application, you can use the same configuration to integrate your existing Ruby on Rails application with FusionAuth.
rails new myapp && cd myapp
Authentication
We’ll use the OmniAuth Library, which simplifies integrating with FusionAuth and creating a secure web application.
Configure Omniauth
Install the omniauth gem and other supporting gems. Add the following three lines to your Gemfile
.
gem "omniauth"
gem "omniauth-rails_csrf_protection"
gem "omniauth_openid_connect"
Then, install them.
bundle install
Next, update your config/environments/development.rb
file with FusionAuth OpenID Connect (OIDC) environment specific configuration.
You’ll have to add similar configuration to the correct environment files when deploying to prod or other environments.
# fusionauth oidc configuration. In production, change issuer to FusionAuth production url
config.x.fusionauth.issuer = "http://localhost:9011"
config.x.fusionauth.client_id = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
Configure Omniauth by creating config/initializers/omniauth.rb
with the following code:
# only if you want a link instead of a button for login
#OmniAuth.config.allowed_request_methods = [:post, :get]
Rails.application.config.middleware.use OmniAuth::Builder do
provider :openid_connect,
name: :fusionauth,
scope: [:openid],
response_type: :code,
issuer: Rails.configuration.x.fusionauth.issuer,
ssl: false,
client_options: {
# discovery doesn't work with local development
authorization_endpoint: Rails.configuration.x.fusionauth.issuer+"/oauth2/authorize",
token_endpoint: Rails.configuration.x.fusionauth.issuer+"/oauth2/token",
userinfo_endpoint: Rails.configuration.x.fusionauth.issuer+"/oauth2/userinfo",
jwks_uri: Rails.configuration.x.fusionauth.issuer+"/.well-known/jwks.json",
identifier: Rails.configuration.x.fusionauth.client_id,
secret: ENV["OP_SECRET_KEY"],
redirect_uri: 'http://localhost:3000/auth/fusionauth/callback',
send_nonce: false
}
end
This pulls values from the environment file and configures the omniauth gem to communicate with FusionAuth.
Add Controllers
Next, you can create some controllers with the following shell commands:
rails generate controller auth
rails generate controller home
rails generate controller make_change
These controllers have the following purposes:
auth
is for omniauth integrationhome
is an unprotected home page with a login buttonmake_change
is a protected page for our example bank application
First, let’s update the config/routes.rb
file. Here’s what that should look like:
Rails.application.routes.draw do
get 'make_change', to: "make_change#index"
get 'logout', to: 'auth#logout'
get 'auth/:provider/callback', to: 'auth#callback'
root to: 'home#index'
end
Some simple routes corresponding to the controllers:
make_change
is the protected bank pagehome
is the home page, which is available to unauthenticated users. This is also the default page.logout
is tied to the auth controller’s logout method.auth/:provider/callback
is the omniauth callback method, which completes the OIDC grant.
Now, update the auth controller at app/controllers/auth_controller.rb
to look like this, which completes some of the routes defined above.
class AuthController < ApplicationController
skip_before_action :authenticate_user!
def logout
session[:user] = nil
redirect_to Rails.configuration.x.fusionauth.issuer+"/oauth2/logout?client_id="+Rails.configuration.x.fusionauth.client_id
end
def callback
session[:user] = request.env['omniauth.auth'].info
redirect_to '/'
end
end
This lets you have a nice logout
method and also handle the callback from omniauth. The latter sets a session
attribute with user data, which can be used by views later.
Now, update the application controller at app/controllers/application_controller.rb
.
class ApplicationController < ActionController::Base
before_action :authenticate_user!
before_action :redirect_non_localhost!
def authenticate_user!
redirect_to '/login' unless session[:user]
end
def redirect_non_localhost!
# Ensure we're on the same hostname/url registered as the callback URL in FA
redirect_to('http://localhost:3000', allow_other_host: true) unless request.host == "localhost"
end
end
authenticate_user!
enforces authentication for all routes in your application by checking for the session attribute set by the auth controller after a successful login.redirect_non_localhost!
ensures users access the web app vialocalhost
instead of a url like http:127.0.0.1:3000/ . FusionAuth’s origin and redirect URL configurations in this example expectlocalhost
. In production, update this with your domain name.
Now, let’s build out the home page. Update the home controller at app/controllers/home_controller.rb
to look like this:
class HomeController < ApplicationController
skip_before_action :authenticate_user!
def index
end
end
You’re skipping authentication for this route. After all, a user has to have someplace to go if they are unauthenticated, right?
Finally, we’ll add some business logic for logged in users to make change with the following code in app/controllers/make_change_controller.rb
:
class MakeChangeController < ApplicationController
def index
if defined? params[:amount]
amount = params[:amount].to_d
@formatted_amount = sprintf("%0.2f", amount)
@nickels = (amount / 0.05).to_i
@pennies = ((amount - 0.05*@nickels) / 0.01).round
end
end
end
App Customization
In this section, you’ll turn your application into a trivial banking application with some styling.
Add Views
The view is welcoming, but prompts them to login. Otherwise, it shows a mock account balance. Replace app/views/home/index.html.erb
with this code:
<% if !session[:user] %>
<h1>Login to manage your account</h1>
<% else %>
<h1>Welcome <%= session[:user]["first_name"] %>!</h1>
<br/>
Your account balance is $100.00
<% end %>
Next, update the layout so the user has login or logout buttons on every page. Add the below code to app/views/layouts/application.html.erb
just after the <body>
tag.
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<%= image_tag "example_bank_logo.svg", class:"headerImage"%>
<% if !session[:user] %>
<%= form_tag('/auth/fusionauth', method: 'post', data: {turbo: false}) do %>
<button type='submit' class='button-lg'>Login</button>
<% end %>
<% else %>
<%= session[:user]["email"] %>
<%= button_to "Logout", '/logout', method:'get', class:'button-lg' %>
<% end %>
</div>
<div id="menu-bar" class="menu-bar">
<% if session[:user] %>
<a href="/" class="menu-link">Account</a>
<a href="/make_change" class="menu-link">Make Change</a>
<% else %>
<a href="/" class="menu-link">Home</a>
<% end %>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="content-container">
<%= yield %>
</div>
<% if !session[:user] %>
<div style="flex: 0;">
<%= image_tag "money.jpg", style:"max-width: 800px;"%>
</div>
<% end %>
</div>
</div>
</div>
Finally, add a form and messaging for making change at app/views/make_change/index.html.erb
:
<h1>We Make Change</h1>
<% if params.has_key?(:amount) %>
<p>We can make change for
$<%= @formatted_amount %>
with
<%= @nickels %> nickels and
<%= @pennies %> pennies!
<% end %>
</p>
<%= form_with url: "/make_change", method: :get do |form| %>
<%= form.label :amount, "Amount in USD: $" %>
<%= form.text_field :amount, placeholder:"0.00", :autofocus=>true %>
<%= form.submit "Make Change" %>
<% end %>
Add Styling
Now, add some image assets and styling to make this look like a real application with the following shell commands:
curl -o app/assets/images/example_bank_logo.svg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/example_bank_logo.svg
curl -o app/assets/images/fusion_auth_logo.svg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/fusion_auth_logo.svg
curl -o app/assets/images/money.jpg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/money.jpg
curl -o app/assets/stylesheets/changebank.css https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/stylesheets/changebank.css
Once you’ve created these files, you can test the application out.
Run the App
Start up the Rails application using this command:
OP_SECRET_KEY="super-secret-secret-that-should-be-regenerated-for-production" bundle exec rails s
OP_SECRET_KEY
is the client secret, which was defined by the FusionAuth Installation via Docker step. You don’t want to commit secrets like this to version control, so use an environment variable.
You can now open up an incognito window and visit the Rails app at http://localhost:3000 . Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button.
Next Steps
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you’re going to want to do.
FusionAuth Customization
FusionAuth gives you the ability to customize just about everything with the user’s experience and your application’s integration. This includes
- Hosted pages such as login, registration, email verification, and many more
- Email templates
- User data and custom claims in access token JWTs
Security
- OmniAuth OpenIDConnect handles token validation and refresh
- You may want to customize the token expiration times and policies in FusionAuth
- Choose password rules and a hashing algorithm that meet your security needs
Tenant and Application Management
- Model your application topology using Applications, Roles, Groups, Entities, and more
- Set up MFA, Social login, and/or SAML integrations
- Integrate with external systems using Webhooks, SCIM, and Lambdas
Troubleshooting
- I get
This site can’t be reached localhost refused to connect.
when I click the Login button
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.
- I get an error page when I click on the Login button with message of
"error_reason" : "invalid_client_id"
Ensure the value for config.x.fusionauth.client_id
in the file config/environments/development.rb
matches client Id configured in FusionAuth for the Example App Application at http://localhost:9011/admin/application/.
- I’m getting an error from Rails after logging in
Rack::OAuth2::Client::Error
invalid_client :: Invalid client authentication credentials.
This indicates that Omniauth is unable to call FusionAuth to validate the returned token. It is likely caused because of an incorrect client secret. Ensure the OP_SECRET_KEY
environment variable used to start rails matches the FusionAuth ExampleApp client secret. You can review that by logging in as the admin user and examining the Application at http://localhost:9011/admin/application/
- It still doesn’t work
You can always pull down a complete running application and compare what’s different.
git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
cd fusionauth-quickstart-ruby-on-rails-web
docker-compose up -d
cd complete-app
bundle install
OP_SECRET_KEY=super-secret-secret-that-should-be-regenerated-for-production bundle exec rails s