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 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 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:
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:
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.
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.
We should adequately support the essential steps in this flow with tests.