web

Ruby on Rails

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.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

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

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:

  • 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 is password.
  • 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 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.

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 integration
  • home is an unprotected home page with a login button
  • make_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 page
  • home 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

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"] %>
          &nbsp;
          <%= 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 to do with the user's experience and the integration of your application. This includes:

Security

Tenant and Application Management

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