JavaScript
Article

OAuth Integration Using Hapi

By Adam Bretz

Securing web resources is often a difficult and daunting task. So much so, that it is often left until the last phase of development and then it’s rushed an not done properly. It’s understandable though; security is a very specialized field in development and most people only give it a passing thought – “yeah this should probably be secured…” So then the developers quickly slap together an ad-hoc security method:

if (password === "password1") {
  setCookie();
}
else {
  send(401);
}

and ship the product full of security holes. That snippet is, hopefully, a gross oversimplification, but the point is still valid.

Thankfully, there are developers out there who spend a lot of their time trying to secure websites and web resources and we can lean on their expertise to help us secure our own projects without having to reinvent the wheel.

In this article, we are going to walk through using OAuth tokens to authenticate users via their GitHub credentials. All of those words together probably sounds extremely difficult, but thanks to a few well documented modules, I think you’ll be surprised how easy it really is.

Prerequisites

It is assumed that the reader:
1. has a functional understanding of working with the hapi server framework.
2. has built web resources in the past.
3. has basic understanding of cookies.
4. has a GitHub account.
5. has a rudimentary understanding of what Oath is and what it is used for (you could start by reading the Wikipedia Article about it).

If any of these assumptions are not true, you are strongly urged to get a handle on the listed prerequisites first, and them come back to learn about securing your webpage.

Getting Started

The first thing you’ll need to do is create a GitHub application. This process will net you both ClientID and ClientSecret – both values you will need to set up OAuth on your web server.

  1. Log into your GitHub account and head over to the settings page (https://github.com/settings/profile)
  2. Click on “Applications”
  3. Push the “Generate new application” button and you will be navigated to a new screen that looks like this:
    Register a new OAuth application
  4. Application Name and Application Description can be anything you want. For Homepage URL and Authorization callback URL, let’s set those to the local server we will be working with. In my example, I will be using port 9001, so set both values to “http://localhost:9001”. My full setup looks like this:
    Completed application configuration
  5. After you push “Register application”, you will be redirected to a new screen that will list both the ClientID and ClientSecret. Make note of these values for later.

Summary

This step was purely administrative. We created a new GitHub application that users will be asked about when they try to log in to your site. Rather than trust http://localhost:9001 with our GitHub credentials, we will trust the GitHub application to authenticate users, and then call back to our website when it’s done.

Planning the Server

Before we start coding, lets come up with a rough outline of what we want our server to do. We will start with four routes for the sake of simplicity: a home route, an account information route, a login route, and a logout route.

In the home route, if the user has been authenticated, let’s print their name, otherwise a generic message. Fo the account route, we will show all the information GitHub sends us. If the user requests the account page without being authenticated first, we will respond with the proper status code of 401. The login route will reach out to GitHub, ask the user for their permission to allow our GitHub application access to some of their account information, and then come back to our local web server. Finally, the logout route will log the user out of our website.

Server Skeleton

Let’s get the boilerplate and routes configuration out of the way first.

var Hapi = require('hapi');
var server = new Hapi.Server();

server.connection({ port: 9001 });

server.register([], function (err) {

    if (err) {
        console.error(err);
        return process.exit(1);
    }

    server.route([{
            method: 'GET',
            path: '/login',
            config: {
                handler: function (request, reply) {

                    // Reach out to GitHub, ask the user for permission for their information
                    // if granted, response with their name
                    reply();
                }
            }
        }, {
            method: 'GET',
            path: '/account',
            config: {
                handler: function (request, reply) {

                    // Show the account information if the have logged in already
                    // otherwise, send a 491
                    reply();
                }
            }
        }, {
            method: 'GET',
            path: '/',
            config: {
                handler: function (request, reply) {

                    // If the user is authenticated reply with their user name
                    // otherwise, replay back with a generic message.
                    reply();
                }
            }
        }, {
            method: 'GET',
            path: '/logout',
            config: {
                handler: function (request, reply) {

                    // Clear the session information
                    reply.redirect();
                }
            }
        }
    ]);
    server.start(function (err) {

        if (err) {
            console.error(err);
            return process.exit(1);
        }

       console.log('Server started at %s', server.info.uri);
    });
});

Listing 1 Skeleton hapi server

Summary

The code above creates a server, a connection on port 9001, and adds some routes with stubbed out handler functions. You’ll notice server.register([], function() {...}, we are passing an empty array. As we continue, we will start adding plugins into hapi, but for the initial boilerplate, we will leave them off. We are using server.route to specify the four routes we wanted to build and pass them path and method string and a config object. The config object is going to be used heavily in the next sections. For now, we are replying back on each route with an empty response. If you start the server, you should see:

Server started at http://hostname.local:9001

You should be able to make GET requests to all of the defined routes and received empty 200 responses.

Nothing in this boilerplate should be surprising if you’ve worked with hapi in the past. If not, head on over to the documentation site here to help clear things up.

Plugging In

One of the best parts of hapi is the plugin system. Plugins allow segments of a hapi application to be segmented in small, portable modules. Almost anything you can do with a hapi server object, you can do with a plugin. You can add routes, extension points, listen for events, create cache segments; even register a view engine unique from the main server object. For more information about plugins, check out the tutorial on hapijs.com.

For this example, we are going to use the bell and hapi-auth-cookie plugins.

bell

bell is a hapi plugin that was built to handle the majority of the tedious handshaking required to integrate with third party OAuth providers. It comes with built in support for the most commonly used OAuth clients (Facebook, Twitter, GitHub, and Google, just to name a few). That means, that the majority of the heavy lifting for OAuth integration with GitHub, is already done. We just need to configure our hapi server to use it.

bell handles all of the back and forth required by OAuth and will only call the associated hapi handler function when the user has been successfully authenticated. Otherwise, hapi will respond with a 401. One thing that is very important to note is that bell does not have any concept of a user session. Meaning once the single request has been authenticated via the third party, that authentication will be lost for subsequent requests. You could use bell to secure all of your routes, but then every single request users make against your web site would require the OAuth dance which would be extremely inefficient. What we need is a way to create a secure cookie that holds the OAuth session information and use that secure cookie to authenticate future requests.

hapi-auth-cookie provides a simple-to-use cookie session management. Users have to be authenticated some other way; all hapi-auth-cookie does is provide an api to get and set encrypted cookies. It has a few other utility functions, but it is important to understand that it doesn’t do any authentication on it’s own.

hapi-auth-cookie extends the hapi request object by adding methods via request.auth.session; specifically request.auth.session.set and request.auth.session.clear. set for creating the secure session cookie and clear to remove it. These methods are added inside an ‘onPreAuth’ sever extension point.

For our server, bell will be responsible for all of the OAuth negotiation and, on success, use hapi-auth-cookie to set an encrypted cookie with request.auth.session.set.

Configuring the Plugins

In the next code section, we are going to fill in the empty register function and configure the two plugins for our sever we started in Figure 1.

var Hapi = require('hapi');
var Bell = require('bell');
var AuthCookie = require('hapi-auth-cookie');

//... refer to Listing 1

server.register([Bell, AuthCookie], function (err) {

    if (err) {
        console.error(err);
        return process.exit(1);
    }

    var authCookieOptions = {
        password: 'cookie-encryption-password', //Password used for encryption
        cookie: 'sitepoint-auth', // Name of cookie to set
        isSecure: false
    };

    server.auth.strategy('site-point-cookie', 'cookie', authCookieOptions);

    var bellAuthOptions = {
        provider: 'github',
        password: 'github-encryption-password', //Password used for encryption
        clientId: 'huU4KjEpMK4TECW',//'YourAppId',
        clientSecret: 'aPywVjShm4aWub7eQ3ub3FbADvTvz9',//'YourAppSecret',
        isSecure: false
    };

    server.auth.strategy('github-oauth', 'bell', bellAuthOptions);

    server.auth.default('site-point-cookie');

    //... refer to Listing 1

Listing 2 Configuring bell and hapi-auth-cookie plugins

Code Explanation

server.register is the entry point for adding plugins to a hapi server. It supports several different function signatures, but for our needs, we will pass an array of objects. Each object must implement a register function which will be called and supplied the current hapi server object. Once all the plugins have been registered, the callback will execute.

We need to take a slight detour here to explain how hapi handles authentication. Authentication with hapi is broken down into two concepts; schemas and strategies. The documentation, here describes it best:

Think of a scheme as a general type of auth, like “basic” or “digest”. A strategy on the other hand, is a pre-configured and named instance of a scheme.

Other than very specific and advanced situations, you will be using pre-built schemes and configuring a specific strategy that is appropriate for your application. An authentication strategy will be used throughout the application to secure resources and is an “instance” of a scheme; a scheme is a means to authenticate requests. Both bell and hapi-auth-cookie register new schemes via server.auth.scheme; the ‘bell’ and ‘cookie’ schemes.

The scheme name is the second parameter to server.auth.strategy. The scheme has to be registered with a hapi server before registering strategies that use it. That is why we need to register the plugins first, and then set up strategies via server.auth.strategy.

In Listing 2, we first register a ‘cookie’ strategy and name it ‘site-point-cookie’. Throughout the code, we will reference ‘site-point-cookie’ to refer to this configured cookie strategy. A full explanation of all the available options can be found here. In our example, we are only using password, cookie, and isSecure. password should be a strong string because it will be used by the iron module to encrypt and decrypt the cookie. cookie is the name of the cookie and isSecure sets the ‘Secure’ option of the resultant Set-Cookie header. This means that this cookie will only be transmitted over HTTPS connections. We are setting this to false for now to make using this example easier, but in general, this should be set to true.

github-oauth

The second, and more interesting strategy is a ‘bell’ type named ‘github-oauth’. Similar to the ‘site-point-cookie’ registration, we pass a name, a scheme, and an options object. The full list of bell strategy options can be found on the bell repo here. provider is set to ‘github’ because bell has built in support for GitHub OAuth integration. It can also be set to an object if you are trying to integrate with a provider unknown to bell. password is the string used to encrypt the temporary cookie during the protocol authorization steps. This cookie only persists during authorization steps, afterwards, it is destroyed. clientId and clientSecret are the values we created way back in the ‘Getting Started’ section. The values in Listing 2 will not work as they are just random gibberish for this example, you will need to plug you own values into the code. Finally, isSecure serves the same function as it does in ‘site-point-cookie’.

Finally, we set the default authentication for the entire server to use our cookie strategy named ‘site-point-cookie’. This is just a convenience setting. It tells hapi to authenticate the request with the ‘site-point-cookie’ strategy for every route added with server.route. This drastically reduces the amount of duplicated configuration options needed per each route.

Making it Work

We are finally done with all the configuration and setup! All that remains is a few lines of logic to wire everything together. Once you see the amount of code required, you’ll see that hapi really is a configuration centric framework. Let’s go through each of the routes in Listing 1 and update the configuration object and handler to function.

log in route

The log in route is the route that needs to reach out to and do the OAuth dance with the GitHub server. Listing 3 shows the updated route config option:

method: 'GET',
path: '/login',
config: {
    auth: 'github-oauth',
    handler: function (request, reply) {

        if (request.auth.isAuthenticated) {

            request.auth.session.set(request.auth.credentials);
            return reply('Hello ' + request.auth.credentials.profile.displayName);
        }

        reply('Not logged in...').code(401);
    }
}

Listing 3 Log in route updates

Only the config option has changed here. First, we want to set the auth option to ‘github-oauth’. This value refers to our ‘bell’ strategy we created in Listing 2 named ‘github-oauth’. This tells hapi to use the ‘github-oauth’ strategy when trying to authenticate this route. It we omit this option, hapi will fall back and use the default strategy we specified in Listing 2; ‘site-point-cookie’. The full list of available auth options is outside the scope of this article, but you can read more about them here.

In the handler function, we check the request.auth.isAuthenticated value of the request . request.auth is added to request only on routes that have authentication enabled. If isAuthenticated is true, we want to set a cookie indicating that. Remember, hapi-auth-cookie added a session object to request.auth with set and clear functions. So now that the user has been authenticated with GitHub, we want to create a session cookie to use throughout the application with request.auth.session.set and pass in the credentials object returned to us from GitHub. This will create an encrypted cookie named ‘sitepoint-auth’ per the options we passed into hapi-auth-cookie. Finally, we want to respond with a little message showing the GitHub display name.

If the user isn’t authenticated or declines GitHub OAuth access, we will respond with a message and a 401 status code.

account route

The account route should show the users GitHub information if they are logged in and if not, respond with a 401. The updated config and handler code is below in Listing 4.

method: 'GET',
path: '/account',
config: {
    handler: function (request, reply) {

        reply(request.auth.credentials.profile);
    }
}

Listing 4 Account route updates

Not many changes in this route. Because we didn’t override any of the auth values in the config object, this route uses the default cookie strategy. When the account route is requested, hapi will look for the ‘sitepoint-auth’ cookie and make sure it exists and is a valid cookie for this request. If it is, the handler will be called, otherwise the response will be a 401. request.auth.credentials is the cookie value we set in the log in route in Listing 3 and profile is where GitHub stores the majority of the user account information.

At this point, you should be able to test the two routes we have added (‘/login’ and ‘/account’) and see how they work together and how they respond.

home route

Like most web sites, we should have a route at the root of the site. Looking back at what we want that route to do, the response should be tailored depending on the users authentication state. The user shouldn’t receive a 401 if they aren’t logged in instead they should see a non-customized home page. If they are logged in, we want to welcome them back with a customized message.

method: 'GET',
path: '/',
config: {
    auth: {
        mode: 'optional'
    },
    handler: function (request, reply) {

        if (request.auth.isAuthenticated) {
            return reply('welcome back ' + request.auth.credentials.profile.displayName);
        }

        reply('hello stranger!');
    }
}

Listing 5 Home route updates

Listing 5 introduces a new concept to the auth setup; mode. The mode value can take one of three string values; ‘required’, ‘optional’, and ‘try’. ‘required’ means that the request must have present and valid authentication. ‘optional’ means that the request doesn’t need to have authentication, but if it does, it must be valid. Finally, ‘try’ is the same as ‘optional’ but the authentication doesn’t have to be valid.

This route has the default cookie strategy we set up in Listing 2, so all we need to do is set the mode and the strategy will be ‘site-point-cookie’. In the handler, we can check the auth status of the request similar to Listing 3. If it’s true, the user has a valid ‘sitepoint-auth’ cookie and we can respond by to the request to the information stored in request.auth.credentials; just like Listing 4. If the auth status is false, we don’t know anything about the user, the handler function will reply with a generic message. Try changing mode to ‘required’ and clearing your cookies to see the difference between ‘required’ and ‘optional’.

logout route

Finally, lets update the log out route to remove the session cookie.

method: 'GET',
path: '/logout',
config: {
    auth: false,
    handler: function (request, reply) {

        request.auth.session.clear();
        reply.redirect('/');
    }
}

Listing 6 Logout route updates

Because we have a default authentication strategy for all routes, we want to disable auth for this route to allow any request through. This is useful to remember if you use a default strategy. Otherwise, you will end up authenticating every single request to your server and you probably don’t want want that; especially for static resources. In the handler we call request.auth.session.clear() which unsets the ‘sitepoint-auth’ cookie and finally we redirect the user back to the root of the site. If the user doesn’t have a the ‘sitepoint-auth’ cookie, this code is essentially a “no-op” but will not hurt anything and is perfectly safe.

Summary

That seems like a lot of words, but the majority of it is explaining configuration options and how some of the hapi authentication internals work. hapi breaks authentication into two concepts; schemes and strategies. A scheme is a general type of authentication and a strategy is a configured instance of a scheme. We used bell to do the OAuth dance with GitHub and we used hapi-auth-cookie to save the user’s GitHub information into an encrypted cookie named ‘sitepoint-auth’. We used this cookie throughout the rest of the application to determine the authentication status.

The majority of the code in the actual route handlers is extremely trivial because the bulk of the heavy lifting is done with the hapi plugins. In the log in route, we set a secure cookie containing all of the information sent from GitHub. In the account resource, the current contents of the cookie are sent back to the user as JSON. In the home route, we changed the authentication mode to allow a mix of no auth and auth, which is a very common scenario for root resources, and responded accordingly. Finally, we completely disabled auth for the logout route and cleared the ‘sitepoint-auth’ cookie and redirected the user to the home page.

Hopefully after reading this article, you’ll see that the majority of the work required is only in configuration. There is very little code beyond the basic hapi boilerplate. I encourage you to check out the full, working code here and experiment on your own with the different options and authentication settings.

More:

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
Jasonspd

Hi there, this is a fantastic and clear tutorial. I've got a problem however and I'm wondering if you could give me suggestions. My session.clear is working, however upon logging in again, it automatically detects my github account again.

I was wondering if it's a github thing because logging out manually on github clearly solves this but defeats the purpose of this tutorial. Any suggestions?

arb

If you want to see the full lifecycle again, you need to go into your GitHub account settings and delete the example app. This will force the app to re-authenticate back to GitHub again. Even with this small hiccup, I don't think it defeats the purpose of this tutorial.

bataille_christophe

Hello,
thanks for this nice and neat tutorial.

Now let's say i want to build a private website with only a selected few people (my team) to access to it and that i need to have different roles for different people.
How do i get it done ? Shall i build a local repository (table) to map users and their roles ? or is this something i can do in GitHub, google, ... ? If i need to add this extra check, what would be the proper key to link the authorized user from the OAuth2 server and my local table ? the email, token, ... ?
And where shall i plug this in the code ?

Sorry it might be just too much questions ... but that's the one i have in mind

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.