Rails 6 splash screen

Authentication is verifying that somebody really 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 fairly 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 dashboard. Army 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 accessible 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:

Rails 6 splash screen

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.

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>

The authentication_path is a Auth0 path that triggers the authentication process which should be created 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

Now our root URL upon refreshing should look like this:

Rails 6 splash screen

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

Rails 6 splash screen

That’s because we’ve created a route for a controller that’s inexistent. Before we create it thought 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

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' } )

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:

Rails 6 splash screen

Once we log in with Google, we should be redirected to the dashboard as was directed in the callback action of the Auth0Controller. I 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 the dashboard to be only accessible by logged in users!

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 controllers that have 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

Quick Tip: In Rails, if an action has no body, you can remove the method entirely. On encountering def index; end, Rails knows to automatically 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

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. Basically, we exposing our methods to all controllers.

# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include HomeHelper

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.

Easy. We now have our /dashboard protected from anyone who is not logged in.

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 may 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 obviously 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 basically 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 own 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

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

The greeting method just picks the logged in user’s name and displays that in the header. Otherwise, the header should just 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

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

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>

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

Rails 6 splash screen

There’s one thing however that’s easy to miss. 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
<% # app/views/auth0/failure.html.erb %> <center> <h1>Sorry, something broke. Please try again later.</h1>

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:

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

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

Rails 6 splash screen

This goes without saying that important steps in this flow should be adequately supported with tests.

We’re done!

Feel free to follow me on Twitter or share and comment.