Chapter 5. Securing Applications with Authentication and Authorization

In this chapter, we're going to explore different methods of securing an application through authentication and authorization. We'll talk about some of the basics of each concept, and then show how hapi simplifies the process of adding both to an application in an easy-to-manage, configurable way.

Fortunately, hapi is a security-focused framework, and as mentioned throughout this book, aims to ensure that developers don't accidentally use the wrong defaults when it comes to implementing things such as security. Therefore, right from the framework's inception, it has had core support for both authentication and authorization, rather than leaving it up to a third-party module. Application security is core to almost every application nowadays; it's not enough for it to be an afterthought in an application or a framework.

When first working with a new technology or framework, security was usually the first stumbling block I arrived at, where either I had to implement my own authentication, which is risky as I'm not a security expert, or I had to use a library which performed some under-the-hood magic which I didn't understand. This meant that I lost the opportunity to learn about the underlying security protocols and implementation. hapi does neither; instead it exposes the tools we need, in an easy-to-use, manageable, and understandable form. Let's look at the way to add authentication and authorization to our applications now, starting with authentication.

Authentication

Authentication is the process of determining whether a user is who they claim to be. For example, for whatever username they supply, they have another determining factor that proves that they are who they say there are. Most often, this is done by supplying a secret that only the user would know, such as a password.

In most applications, this username and password combination will return or create a token that will be stored somewhere with the user, so all future interactions within the application won't need to be re-authenticated with the same username and password. This token is usually stored in a cookie.

In both cases, we would usually take the password, token, or any other form of access key from the request to our application by parsing headers or cookies, depending on the type of authentication, and compare it with some data which is stored in our database. For those of you familiar with authentication, you may recognize the authentication protocols that have been described just now as basic and cookie.

Both these protocols can be described in two stages. The first stage is the acquisition of the token, or the underlying mechanics of the authentication protocol such as parsing the authorization header of a request. In hapi, this is called a scheme. To create an authentication scheme in hapi, you would use the server.auth.scheme() API as follows:

…
server.auth.scheme('basic', (server, options) => {
  // do token parsing logic here
});
…

Fortunately, there are plugins in the hapi ecosystem that cover most types of authentication schemes for you, and register the authentication scheme when you register the plugin. Unless you are writing a new authentication protocol, you will likely never need to write a scheme for your hapi application, but it's good to know what these authentication scheme plugins do under the hood. We'll explore examples demonstrating these plugins in use later in this chapter.

The second stage is where we take our token, or the username and password combination, and compare it with something in our database to check if they are valid. In hapi, this is what we call a strategy. Unlike schemes, your strategies will be application-specific, and will likely be implemented as per application. Strategies are registered using the server.auth.strategy() API as follows:

…
const validate = function (request, username, password, next) {
  // perform validation logic here
};
const basicConfiguration = { validateFunc: validate };
server.auth.strategy('simple', 'basic', basicConfiguration);
…

There are a few things happening in this preceding example, so let's go through it in more detail. When registering a strategy, we supply the following:

  • The name of the strategy we are creating; here it is simple.
  • The name of the scheme that the strategy will use, which has previously been registered through server.auth.scheme(). Here the scheme used is basic.
  • An object containing the required configuration to validate credentials, which is specific to the scheme. With the basic authentication scheme, all that is required is an object with a function called validateFunc that validates credentials.

It's worth knowing that we can actually register multiple schemes and strategies within our application. This is very common and is very useful when applying one strategy to the API endpoints, such as a Bearer token or OAuth workflow, and a different strategy for authentication when viewing templates or views within an application, where something like cookie-based authentication would be more suitable.

Following the registering of our strategy, we then have multiple ways of applying a particular authentication strategy to our application. We can apply it through the route configuration object if we need a route-specific authentication, or set the strategy to be applied to all our routes as the default.

This is a lot to take in at once and may still be a little unclear, but it will hopefully become much easier to understand when we go through a few examples using different forms of authentication. We will also look at how to configure authentication strategies globally or to specific routes. Let's look at a few examples of registering some authentication schemes and strategies now. If you want to run any of these examples yourself, all these examples are available in the source code supplied with this book, which can be found in the GitHub repository available at https://github.com/johnbrett/Getting-Started-with-hapi.js.

Configuring authentication

Let's look at adding authentication to a server now. The first example will use the basic authentication scheme. Aptly named, the basic authentication scheme is the simplest authentication scheme to use for an app, and is a great place to start when learning about authentication.

The following example shows us the multiple steps of adding an authentication scheme to an application in hapi. First we install the hapi-auth-basic plugin (https://github.com/hapijs/hapi-auth-basic) from npm:

$ npm install hapi-auth-basic --save

In our application code, we then require the plugin which, when registered, will register the basic scheme. We then add a strategy and apply it to our routes. Let's look at what that looks like now. I've broken this code example into the following two files, so it is easier to digest:

  • index.js: It contains all the authentication configuration and server logic
  • routes.js: It contains some route configuration objects

Let's go through what this looks like now:

const Hapi = require('hapi');
const Basic = require('hapi-auth-basic');                 // [1]
const Blipp = require('blipp');
const routes = require('./routes');
const server = new Hapi.Server();
server.connection({ port: 1337 });
server.register([
  Basic,                                                  // [2]
  { register: Blipp, options: { showAuth: true } }        // [3]
], (err) => {
    // handler err
    const basicConfig = {
      validateFunc: function (request, username, password, callback) {
        if (username !== 'admin' || password !== 'password') {
          return callback(null, false);
        }
        return callback(null, true, { username: 'admin' });
      }
    };
    server.auth.strategy('simple', 'basic', basicConfig); // [4]
    server.auth.default('simple');                        // [5]
    server.route(routes);                                 // [6]
    server.start(() => {});                               // [7]
});

With reference to the numbers in the comments in the preceding code, let's go through the explanation:

  • [1]: We require the hapi-auth-basic module, and store it in the variable Basic.
  • [2]: We register basic, the authentication scheme.
  • [3]: We also register blipp, but this time configure it to display authentication information for our routes as well as the routing table.
  • [4]: We set our strategy for the basic authentication scheme. Here, we specify the name as simple; it uses the basic authentication scheme, which has been already registered, and pass it the configuration object that the basic authentication scheme expects. This configuration object is just an object with the function validateFunc that performs the actual validation of credentials when passed from a request. More on that in a bit.
  • [5]: We set the simple authentication strategy we just registered to be required on all routes on our server. This can and will be overwritten in the route-specific configuration when we look at our routes.js file.
  • [6]: We add our routes.
  • [7]: I've just trimmed down our server start for brevity, but always remember to handle errors here; it will save hours of debugging if you ever find yourself with a server not starting without an error!

Hopefully, that was easy enough to follow. Now let's look at the validateFunc function of our basic config in more detail. You see this has our request, username, password, and callback parameters. While all the other parameters are self-explanatory, the callback here is worth explaining. The callback here is in the form:

callback(error, isAuthenticated, credentials)

The explanation of these parameters are as follows:

  • error: There was an error trying to valid the credentials of the user
  • isAuthenticated: The user was successfully authenticated from the given credentials
  • credentials: User information; this accepts an object, and will attach any values here to the request.auth.credentials, which can be used later in the request life cycle such as in our handlers and route prerequisites

If an error is passed back, it will be treated as most errors in hapi: a boom error will be sent as is, whereas an unwrapped Error object will result in an internal server error.

If we run this example, we now get the authentication info as well from blipp. Let's see what this looks like:

Configuring authentication

Straight away, we can see our routes with authentication info applied, but one of our routes has the authentication simple applied, while the other doesn't! Let's look at routes.js to see why that might be:

module.exports = [
  {
    method: 'GET',
    path: '/public',
    config: {
      auth: false,
      handler: function (request, reply) {
        return reply(request.auth);
      }
    }
  },
  {
    method: 'GET',
    path: '/private',
    config: {
      handler: function (request, reply) {
        return reply(request.auth);
      }
    }
  }
];

If you look at the public route, there's a new property added inside config object, auth, which is set to false. This basically registers the route, but applies no authentication strategy to it, which explains why there is no authentication applied to our /public route.

Noticing that both routes return the auth information for a request in their reply, try running this example, visiting the /public and /private routes, and taking note of the responses. What you will see if you try accessing the /private route, while unauthenticated, is an HTTP-friendly 404 unauthorized error, via the boom module.

What you saw in the preceding code, with auth set to false on the /public route, was the authentication mode applied to this route. There are actually a number of authentication modes that can be set for a strategy or route, such as the following:

  • false: It means no authentication is applied to this route
  • required: It means credentials must be present and are valid
  • try: It means if credentials are present, attempt a login, but continue with request life cycle even if the attempt fails
  • optional: It means if credentials are present, attempt to authenticate; if it fails, the request fails

Authentication modes are quite convenient when it comes to login and logout pages that may or may not have an authentication strategy applied.

There is a major downfall with the basic authentication protocol, which you may have already spotted; for this type of authentication, user credentials are required for every request—one of the reasons why it is rarely used in applications. A much more common approach is to have the user enter credentials once, and then use a cookie to store their credentials for future requests. Let's look at how we would do this now.

Cookie authentication

Probably the most common authentication scheme for websites and web applications is to use cookies for storing user credentials and security tokens. As hapi-auth-basic provides us with the basic authentication scheme, hapi-auth-cookie (https://github.com/hapijs/hapi-auth-cookie) provides us with a cookie-based authentication scheme.

Cookie-based authentication is slightly more complicated than our previous example with basic, as we have to manage sessions. With basic auth, we had to provide credentials with every request. In cookie auth, we set the cookie and then we can visit multiple pages without re-authenticating until that cookie expires.

To deal with this session handling, hapi-auth-cookie decorates the request.auth.session object with methods to set and clear the cookies. Let's look at an example of hapi-auth-cookie in action. Again, we'll split our example into an index file for all the server and authentication logic, and a routes file for all our route configuration. Let's look at what that looks like now. First, index.js:

const Hapi = require('hapi');
const Cookie = require('hapi-auth-cookie');
const Blipp = require('blipp');
const routes = require('./routes');
const server = new Hapi.Server();
server.connection({ port: 1337 });
server.register([
  Cookie,
  Blipp
], (err) => {
  // handler err
  server.auth.strategy(
    'session',
    'cookie',
    {
      cookie: 'example',
      password: 'secret',
      isSecure: false,
      redirectTo: '/login',
      redirectOnTry: false
    }
  );
  server.auth.default('session');
  server.route(routes);
  server.start(() => {});
});

In this example, you'll notice that we don't have a validateFunc function in our authentication configuration object. This is because with hapi-auth-cookie, it's our responsibility to validate the credentials and then call request.auth.session.set() to create a session if that user has been successfully authenticated.

In the authentication scheme configuration object, the properties are as follows:

  • cookie: This is the name of the cookie.
  • password: This is used to encrypt the cookie with iron (https://github.com/hueniverse/iron).
  • isSecure: This checks whether the cookie is allowed to be transmitted over insecure connections (you should only set this to false during development).
  • redirectTo: This is a location to redirect unauthenticated requests to. This is useful for simplifying handler logic, so we don't have to worry about redirects for unauthenticated users.
  • redirectOnTry: This configures whether to attempt redirecting for requests where route authentication is in the try mode. The try mode configures a route so that if a cookie doesn't exist, the code proceeds without attempting to authenticate. If one does, it attempts to authenticate the user. We will see how this is used in the next example.

Now that we have our cookie authentication scheme and strategy registered, let's apply it to some routes and see what this looks like. This will be in our routes.js file:

module.exports = [
  {
    method: 'GET',
    path: '/login',
    config: {
      auth: {
        mode: 'try'
      },
      handler: function (request, reply) {
        if (request.auth.isAuthenticated === true) {
          return reply.redirect('/private');
        }
        let loginForm = `
          <form method="post" action="/login">
            Username: <input type="text" name="username" />
            <br>
            Password: <input type="password" name="password" />
            <br>
            <input type="submit" value="Login" />
          </form>
          `;
          if (request.query.login === 'failed') {
            loginForm += `<h3>Previous login attempt failed</h3>`;
          }
          return reply(loginForm);
        }
      }
    },
    {
      method: 'POST',
      path: '/login',
      config: {
        auth: {
          mode: 'try'
        },
        handler: function (request, reply) {
          if (request.payload.username !== 'admin' ||
            request.payload.password !== 'password') {
            request.auth.session.clear();
            return reply.redirect('/login?login=failed');
          }
          request.auth.session.set({
            username: request.payload.username,
              lastLogin: new Date()
          });
          return reply.redirect('/private');
        }
      }
    },
    {
      method: 'GET',
      path: '/public',
      config: {
        auth: {
          mode: 'try'
        },
        handler: function (request, reply) {
          return reply(request.auth);
        }
      }
    },
    {
      method: 'GET',
      path: '/private',
      config: {
        handler: function (request, reply) {
          return reply(request.auth);
        }
      }
    }
];

Here we have added four routes as displayed in our blipp output:

Cookie authentication

You might have noticed that all routes have the session authentication strategy applied to them in this example, although some routes use the try mode, I'll explain how that is useful in a bit. First, let's go through what is happening in each route here.

We first have two /login routes, one being a GET and one a POST route. The GET /login route will return the HTML for our login form, but has some extra logic in there. If the client is already authenticated and tries to access the login route, the authenticated client will be redirected to the /private route. This is where the try mode is useful; if credentials exist, they will be checked and we can redirect if necessary. If not, we just display the login form. We also check if there has been an unsuccessful authentication attempt, and if so, display a message to the user.

The POST /login route is where the actual authentication process takes place. Here we compare the payload parameters against our hardcoded strings. If this fails, we clear any session cookies, and redirect the client back to the login page with the query parameter set so that we will show the login failed message mentioned in the GET /login route.

Next is the GET /public route. This just displays the current authentication information. You should run this code example, and visit the /public route once while unauthenticated and once after authenticating to note the differences in the request.auth object that is returned. You might be curious why the try authentication mode would be used here in the route configuration object. If you think of a website sucha as Amazon, you can view product pages while not signed in, and in the top right of the page you see a message like Sign in, but if a cookie exists with the correct credentials, you would see a message like Hello User. The try authentication mode makes this functionality very easy; if no cookie exists, show the normal page; if one does, authenticate the user and perform any required customizations to the required page.

Finally, we have the GET /private route. Unlike the public route, where a client may or may not be authenticated, this route requires the user to be successfully authenticated, or they will be redirected to the login page.

It is worth taking some time here to look in depth at the source code associated with the book for this chapter, and test this example. Try visiting each route while authenticated and unauthenticated. A good exercise to test your knowledge of authentication so far would be to try and add a GET /logout route to this example.

Third-party authentication

It is becoming more and more common in web applications to use a third party, such as Twitter or GitHub, for authentication. Using third parties for authentication means potential users for your application can just use the credentials and profile of an existing service to save them from completing tedious signup forms.

This, much like with the other authentication examples we've seen, are made much easier to implement using a hapi plugin. The plugin for third-party authentication is called bell (https://github.com/hapijs/bell). It is worth noting that bell doesn't support any kind of session management, so it is usually combined with hapi-auth-cookie to form a full authentication solution.

Let's try to build an application now that will use two authentication strategies to form the full authentication flow. We will use bell to authenticate a user with Twitter and then hapi-auth-cookie to maintain the session. Let's see what this looks like, again splitting the application into two files: index.js for the server and authentication logic, and routes.js for our route configuration. Let's look at what our index.js would look like:

const Hapi = require('hapi');
const Cookie = require('hapi-auth-cookie');
const Bell = require('bell');
const Blipp = require('blipp');
const routes = require('./routes');
const server = new Hapi.Server();
server.connection({ host: '127.0.0.1', port: 1337 });
server.register([
  Cookie,
  Bell,
  { register: Blipp, options: { showAuth: true } }
], (err) => {
  // handle err logic
  server.auth.strategy(
    'session',
    'cookie',
    {
      cookie: 'example',
      password: 'password',
      isSecure: false,
      redirectTo: '/login',
      redirectOnTry: false
    }
  );
  // Acquire the clientId and clientSecret by creating a
  // twitter application at https://apps.twitter.com/app/new
  server.auth.strategy(
    'twitter',
    'bell',
    {
      provider: 'twitter',
      password: 'cookie_encryption_password',
      clientId: '',
      clientSecret: '',
      isSecure: false
    }
  );
  server.route(routes);
  server.start(() => {});
});

Hopefully, all this looks familiar, except that we have now added in the bell plugin, registered it, and created a second strategy that uses Twitter for authentication with application credentials obtained by creating an application on the Twitter developer site, https://apps.twitter.com/app/new.

Next, let's add some routes to authenticate with Twitter, and store this info in a cookie; this part will be in our route.js file:

module.exports = [
  {
    method: 'GET',
    path: '/login',
    config: {
      auth: 'twitter',
      handler: function (request, reply) {
        if (!request.auth.isAuthenticated) {
          request.auth.session.clear();
          return reply('Login failed...');
        }
        request.auth.session.set({
          username: request.auth.credentials.profile.username
        });
        return reply.redirect('/private');
      }
    }
  },
  {
    method: 'GET',
    path: '/private',
    config: {
      auth: 'session',
      handler: function (request, reply) {
        return reply(request.auth);
      }
    }
  }
];

Surprisingly, not too much code is required to achieve a session-based login system using a third party this time. Let's go through the function of each route now.

First we have the GET /login route. This route, when going through the authentication stage of the request life cycle, will prompt the client to log in to Twitter. Upon successful authentication, the request.auth.credentials will be populated with the response from the Twitter authentication step. From this, we take the username from the Twitter response, store it in our credentials object for our session and call request.auth.session.set() to create our session, and finally, redirect to our /private route.

Our private route which is only accessible to an authenticated user, is now accessible and displays the authentication details for the request, complete with the client's username. If you delete the cookie and try accessing this route again, you will be redirected to the login route as specified in the cookie authentication strategy configuration object.

As a good exercise to solidify your knowledge of authentication, I recommend trying to implement third-party authentication with a different service provider than Twitter, such as GitHub. The third-party authentication workflow can be difficult to grasp, so I recommend running this example, and going through the twitter workflow at the very least.

Authentication summary

So far in this chapter, we covered the concepts of schemes and strategies that hapi uses to implement authentication. We looked in depth at examples of different authentication workflows such as basic cookie-based session authentication, and using third-party providers for authentication.

We looked at the different authentication modes included with hapi, such as try, optional, and required, the cases where they are useful, configuring them as default or route-specific for a server, and also accessing authentication information and credentials in our route handlers through request.auth.

Hopefully, this section has given you a good grasp of how authentication works in hapi, and how its core support and modules, covering most types of authentication, greatly simplify the different types of authentication workflow that your application may need.

In the next section, we will look at authorization in hapi and through hapi scopes, and implementing basic permission systems, a very common feature in most applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.172.115