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
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.
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.
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:
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
Now we can add our Auth0 controller, with a
failure actions to match the routes we created.
The content of our
app/controllers/auth0_controller.rb should look like this:
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:
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 template
app/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.
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
dashboard actions, and the actions we want to protect is
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
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.
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.
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
Wait. How about 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
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.
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.
routes.rb should now look like this:
Now we can
reset_sesssion when the
logout route hist it’s action in the controller.
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
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.
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.
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:
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.