Authentication is verifying that somebody is who they claim to be. Using Auth0, a third-party service, let’s implement an identification process for users of our application.

Auth0 provides a universal authentication & authorization platform for web, mobile and legacy applications. They have gems that integrate nicely with Rails to accomplish this. To start, you’ll need to get your application keys. Talking of keys, you may want to set yours up using the somewhat new “encrypted credentials” feature since Rails 5.1.

Stefan Wintermeyer has a nice post about setting this up.

I have put the completed version of everything in this post on GitHub.

Create a Rails app with rails new army (let army be the name of our app). Then cd into it. With our app, we generated a controller with rails g controller home index dashboardArmy is our imaginary application to recruit soldiers. home will be our controller while index and dashboard will be two actions; the former being accessible to anyone who visits our URL (public) and the latter being only available to users who successfully verify their identity – The one we need to protect. Auth0 allows us to identify users with social providers such as Twitter, Facebook, Google, etc.

With our controller in place, when you start the Rails server you’ll see this:

Looks nice but this is not what we want. We want our index page to show (this is the public page with a login button to allow authentication). Let’s create it.

Implementing Login/Authentication

Our app/views/home/index.html.erb is prefilled with some HTML.

Note that we’re using Bootstrap to style our pages and unless you’re using this gem too, your pages may look different.

<div class="jumbotron">
  <center>
    <h1>Republic of Wakanda Army</h1>

    <%= link_to 'Login', authentication_path, class: "btn btn-primary btn-lg" %>
  </center>
</div>

The authentication_path is an Auth0 path that triggers the authentication process, which we should create as shown below. The routes.rb file should contain a route for the auth0 callback for when authentication succeeds and another route for when it fails.

With this let’s modify our config/routes.rb so it looks like this:

Rails.application.routes.draw do
  root 'home#index'

  get 'dashboard' => 'home#dashboard'

  get 'auth/auth0', as: 'authentication'        # Triggers authentication process
  get 'auth/auth0/callback' => 'auth0#callback' # Authentication successful
  get 'auth/failure' => 'auth0#failure'         # Authentication fail
end

Now our root URL upon refreshing should look like this:

As it stands now, when we click on the login button, we’ll get a Routing Error.

That’s because we’ve created a route for a controller that’s inexistent. Before we create it though, we’ll need to add the Auth0 gem that will make this whole authentication process work.

Add the gem below to the Gemfile and run bundle install

gem 'omniauth-auth0', '~> 2.2'

Now we can add our Auth0 controller, with a callback and failure actions to match the routes we created.

The content of our app/controllers/auth0_controller.rb should look like this:

class Auth0Controller < ApplicationController
  # Set session[:userinfo] when authentication succeeds
  def callback
    session[:userinfo] = request.env['omniauth.auth']

    redirect_to '/dashboard'
  end

  # Render failure when something goes wrong.
  def failure
  end
end

We also need an initializer file, config/initializers/auth0.rb that will house our keys to communicate with Auth0’s API.

Our initializer file should look like this:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :auth0,
    Rails.application.credentials.dig(:auth0, :client_id),
    Rails.application.credentials.dig(:auth0, :client_secret),
    Rails.application.credentials.dig(:auth0, :domain),
    callback_path: '/auth/auth0/callback',
    authorize_params: {
      scope: 'openid profile'
    }
  )
end

For good measure, restart your Rails server, navigate to localhost:3000 so see the home page with the login button. If you press the login in button, you should see a page that looks like:

Once we log in with Google, the callback action of the Auth0Controller will redirect us to the dashboard. I have filled the dashboard templateapp/views/home/dashboard.html.erb with some HTML to look like the following:

Rails 6 splash screen

So far, so good.

But there’s a problem. When you clear your cookies or use another browser and navigate to http://localhost:3000/dashboard, you can access the page. But we want only users who have logged in to be able to access the dashboard.

Protecting Pages

To protect pages in our application, we need to protect some actions in our controller because actions render templates and route paths are directed to actions. The controller we’re interested in here is the HomeController, this is the controller that has the index and dashboard actions, and the actions we want to protect is dashboard.

# app/controllers/home_controller.rb

class HomeController < ApplicationController
  before_action :authenticate_user!, only: [:dashboard]

  def index
  end

  def dashboard
  end
end

Quick Tip: In Rails, if an action hasn’t got a body, you can remove the method entirely. When we hit the index action, Rails automagically knows to render :index.

before_action is a filter that runs before a controller action. Here we’re saying we want the authenticate_user! method to be run before the dashboard template is loaded. Inside this method, we’ll check whether or not a user has verified their identity through Auth0. We know a user has logged in when the session has a key of 'userinfo' set, in other words when session['userinfo'] is present. If the user hasn’t verified their identity, we redirect them back to the root URL. Couldn’t be simpler. You could spice this up with flash messages but for this tutorial, this would be an overkill.

Let’s write the authenticate_user! method along with another method user_signed_in? that will check if Auth0 has set session['userinfo'] for a user.

# app/helpers/home_helper.rb
# Ideally we'd call this AuthenticationHelper and place it
# in a file, app/helpers/authentication_helper.rb

module HomeHelper
  def user_signed_in?
    session['userinfo'].present?
  end

  def authenticate_user!
    if user_signed_in?
      @current_user = session['userinfo']
    else
      redirect_to root_path
    end
  end
end

For this to work, however, we need to make these methods available to the HomeController, we can do that by just including this module in the ApplicationController that all controllers inherit from. We are exposing our methods to all controllers.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include HomeHelper
end

Now if you navigate to localhost:3000/dashboard without logging in, you’ll be redirected back to the root URL and be presented again with the login button.

Now any logged out user will not be able to access our /dashboard.

Wait. How about logout?

Implementing Logout

Login or authentication involved setting session['userinfo'] for a user. To log out, we’ll have to set this value to nil; this would be enough to log out with Auth0. Rails has a reset_session method that we could have used, but the problem with this approach is that in case you’d be storing some extra stuff that a user may want to hold on to, for example, if this were a shopping app, we might want to store items in a user’s shopping cart in the session too if they log out, Rails’ reset_session will clear out everything in the user’s session and items they added in their cart will be gone when they log back in next time. The user will have to re-add their items or forget it entirely; this might be bad for sales and not a good user experience, most importantly.

For our app’s purpose, we’ll implement our own reset_session just to clear Auth0 data. There’s a session fixation security issue involving storing session data you might want to consider, but that’s beyond the scope of this tutorial.

Before starting with implementing logout, we need some more helper methods, though. One of these will be current_user which is the session['userinfo'] value from Auth0; this is what identifies a user with his information. Let’s add this method to our HomeHelper, while at it we can also add our implementation of reset_session method.

# app/controllers/home_controller.rb
module HomeHelper
  def user_signed_in?
    session['userinfo'].present?
  end

  def authenticate_user!
    if user_signed_in?
      @current_user = session['userinfo']
    else
      redirect_to root_path
    end
  end

  def current_user
    @current_user
  end

  def reset_session
    session['userinfo'] = nil if session['userinfo'].present?
  end
end

Let’s create more helper methods. Notice how we separated these methods from the HomeHelper method? This just a way to separate concerns or personal preference if you’d like, but it’s more of good practice to group related methods.

# app/helpers/view_helper.rb
module ViewHelper
  def greeting
    if current_user.present?
      @greeting = "Welcome, #{current_user['info']['name'].split.first}!"
      @link = dashboard_path
    else
      @greeting = 'Royal Army of Wakanda'
      @link = root_path
    end
  end

  def login_or_out
    if current_user.present?
      link_to('Log Out', logout_path, class: 'nav-link')
    else
      link_to('Log In', authentication_path, class: 'nav-link')
    end
  end
end

The greeting method just picks the logged-in user’s name and displays that in the header. Otherwise, the header should only have “Royal Army of Wakanda”. We’re also using the @link variable to store a default URL for when the header is clicked, this may not be necessary, but it’s common practice. login_or_out helps us log in and out of the application. But we can’t do that without a logout path. Let’s add a new route to log out and also a controller action to handle logging out.

Our routes.rb should now look like this:

Rails.application.routes.draw do
  root 'home#index'

  get 'dashboard' => 'home#dashboard'

  get '/logout' => 'auth0#logout'

  get '/auth/auth0', as: 'authentication'
  get '/auth/auth0/callback' => 'auth0#callback' #Authentication successful
  get '/auth/failure' => 'auth0#failure'         #Authentication fail
end

Now we can reset_sesssion when the logout route hist it’s action in the controller.

# app/controllers/auth0_controller.rb

class Auth0Controller < ApplicationController
  def callback
    session[:userinfo] = request.env['omniauth.auth']

    redirect_to '/dashboard'
  end

  def logout
    reset_session
    redirect_to root_path
  end
end

Now when a user logs in, they should have a ‘logout’ button and a friendly greeting that may look like this:

Rails 6 splash screen

The header, log out and greeting code we’ll place in app/views/layouts/application.html.erb.

<!DOCTYPE html>
<html>
  <head>
    <title>Army</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body class="universal_padding">
    <% greeting %>
    <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
      <a class="navbar-brand" href="<%= @link %>">
        <%= @greeting  %>
      </a>

      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item active">
            <%= login_or_out %>
          </li>
        </ul>
      </div>
    </nav>

    <%= yield %>
  </body>
</html>

Congratulations. We now have a fully functional authentication system in our Rails app.

Rails 6 splash screen

There’s one thing we can miss easily. What if for some reason Auth0’s API fails? We should be able to communicate to the user we have a problem.

Handling Failure

Let’s create an action in the Auth0Controller and a corresponding app/views/auth0/failure.html.erb. We already have a route for failure get '/auth/failure' => 'auth0#failure'. Let’s add a body to make it more meaningful.

# app/controllers/auth0_controller.rb

class Auth0Controller < ApplicationController
  def callback
    session[:userinfo] = request.env['omniauth.auth']

    redirect_to '/dashboard'
  end

  def failure
    error_msg = request.env['omniauth.error']
    error_type = request.env['omniauth.error.type']

    # It's up to you what you want to do with the error information
    # You could display it to the user or log it somehow.
    Rails.logger.debug("Auth0 Error: #{error_msg}. Error Type: #{error_type}")

    render :failure
  end

  def logout
    reset_session
    redirect_to root_path
  end
end
<% # app/views/auth0/failure.html.erb %>

<center>
  <h1>Sorry, something broke. Please try again later.</h1>
</center>

We have a route, an action and a page to render when something goes wrong with Auth0. What’s left is to instruct Omniauth (OmniAuth is a flexible authentication system utilizing Rack middleware) how to handle errors. We’ll do this by creating yet another file with the following content:

# /config/initializers/omniauth.rb

OmniAuth.config.on_failure = Auth0Controller.action(:failure)

To test this manually and make sure the failure page is rendered, edit your Auth0 client secret, restart the server and you should see the failure page.

Rails 6 splash screen

We should adequately support the essential steps in this flow with tests.

Last Update: January 07, 2024