Ruby: This will be needed for pulling down the various dependencies.
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.
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.
In this section, you’ll get FusionAuth up and running and use Rails to create a new application.
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
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.
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 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:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI 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. 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.
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
We’ll use the OmniAuth Library, which simplifies integrating with FusionAuth and creating a secure web application.
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, :email, :profile],
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.
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 applicationFirst, 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 via localhost
instead of a URL like http:127.0.0.1:3000/ . FusionAuth’s origin and redirect URL configurations in this example expect localhost
. 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
In this section, you’ll turn your application into a trivial banking application with some styling.
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 %>
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.
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.
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 gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
This site can’t be reached localhost refused to connect.
when I click the Login buttonEnsure 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.
"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/.
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/
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