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 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:
simple.
server.auth.scheme()
. Here the scheme used is basic
.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.
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 logicroutes.js
: It contains some route configuration objectsLet'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 userisAuthenticated
: The user was successfully authenticated from the given credentialscredentials
: 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 prerequisitesIf 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:
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 routerequired
: It means credentials must be present and are validtry
: It means if credentials are present, attempt a login, but continue with request life cycle even if the attempt failsoptional
: It means if credentials are present, attempt to authenticate; if it fails, the request failsAuthentication 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.
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:
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.
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.
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.
18.118.122.244