hapi — Basic Authentication With Username and Password

Building web apps is often coupled with user handling which is mostly combined with authentication via username and password. Each user should be identifiable with its own credentials and, you know, the common practice throughout the web that makes use of basic authentication. This tutorial will guide you through the setup of your hapi server to add the functionality of basic authentication with username and password.

Before diving into the details, have a look at the series outline and find posts that match your interests and needs.

hapi Series Overview

Authentication in Hapi

The concept of authentication in hapi is based on schemes and strategies. Picture yourself that schemes are general types of authentication like basic or digest. In reference, a strategy is the actual named instance or implementation of an authentication scheme. We’ll dive deeper into the general topic of authentication within hapi later within this series and explain more details on a practical example by implementing our own custom strategy.

As of now, it’s important to know that hapi uses a scheme and strategy mechanism for authentication.

Basic Authentication With Hapi

You may have suggested that we won’t implement the functionality for basic authentication all by ourselves. There’s already a plugin called hapi-auth-basic that is actively maintained and does its job very well.

At first, add hapi-auth-basic as a project dependency and install it. You can combine both steps by executing the following snippet within your command line:

npm i -S hapi-auth-basic  

Now that you have the plugin for basic auth installed, go ahead and register it to your hapi server instance and afterwards define it as an available authentication mechanism.

const Hapi = require('hapi')  
const BasicAuth = require('hapi-auth-basic')

const server = new Hapi.Server()

async function liftOff () {  
  await server.register({
    plugin: require('hapi-auth-basic')
  }

  server.auth.strategy('simple', 'basic', {
    validate: async (request, username, password) => {
      // TODO
    }
  })
}

liftOff()  

We assume you’re familiar with hapi plugins and how to install them. If not, follow our tutorial on extending your hapi server’s functionalities with plugins within this hapi series and get a fundamental understanding.

Register the hapi-auth-basic plugin using server.register(require('hapi-auth-basic')).

Internally, the plugin defines a new scheme named basic to your hapi server by calling server.auth.scheme(). By now, you’re able to make use of this scheme and create a strategy that is based on the recently added basic scheme. Hopefully this abstract context will make sense for you in a second :)

To create an authentication strategy for your hapi server, leverage the server.auth.strategy(name, scheme, options) function that expects three (optionally four) parameters:

  1. name: your strategy name that will be used throughout your app
  2. scheme: the scheme name on which the strategy is based upon (e.g. basic)
  3. options: additional options

Within the code snippet above, you can see that the basic scheme is used to create a new strategy called simple. You could use the same name for strategy and scheme. For this tutorial, it’s a lot easier to keep context when choosing different names.

Hapi Basic Authentication — Options

The following list outlines available options to customize the behavior of hapi-auth-basic:

  • validateFunc: a required function that will check the provided credentials against your user database
  • allowEmptyUsername: boolean value that indicates whether users are allowed to make requests without a username; defaults to false
  • unauthorizedAttributes: object that will be passed to Boom.unauthorized if set. If there’s a custom error defined, this object will be ignored in favor of the custom error data; defaults to undefined

There’s a required option that you need to provide: validateFunc. That’s a function with the signature function(request, username, password), where

  • request: is the hapi request object which requires authentication
  • username: username send by the client
  • password: password send by the client

The return valie for validateFunc is an object, containing the following fields:

  • err: internal error object that replaces the default Boom.unauthorized if defined
  • isValid: boolean that indicates if the username was found and passwords match
  • credentials: user’s credentials, only included if isValid is true

The following section will show you an exemplary validation function.

Validate User Credentials

Routes that require authentication will be checked against the defined authentication strategy using the provided validation function.

For the password comparison within the exemplary validation function, you’re leveraging the bcrypt library and its provided functionality. If you want to use bcrypt for your project as well, install and add it as a project dependency:

npm i -S bcrypt  

The following code snippet is just for illustration purposes and uses a hard-coded users object. Actually, you would query your database for a username match and compare the provided passwords.

const Bcrypt = require('bcrypt')

// hardcoded users object … just for illustration purposes
const users = {  
  future: {
    id: '1',
    username: 'future',
    password: '$2a$04$YPy8WdAtWswed8b9MfKixebJkVUhEZxQCrExQaxzhcdR2xMmpSJiG'  // 'studio'
  }
}

// validation function used for hapi-auth-basic
const basicValidation = async function (request, username, password) {  
  const user = users[ username ]

  if (!user) {
    return { isValid: false }
  }

  const isValid = await Bcrypt.compare(password, user.password)

  return { isValid, credentials: { id: user.id, username: user.username } }
}

As you can see, there is no magic involved to validate the user input. First, you get the user data from the users object and if there is data available you need to check if passwords match. Of course, you can extend the functionality within the validation function to your needs and proceed with further security checks.

Notice: Bcrypt.compare(candidate_plain_text_password, hash) compares the candidate password provided in plain text with a given hash. You don’t need to hash your candidate before checking if they match.

Route Configuration to Require Authentication

Hapi doesn’t require authentication for every route by default. You have to secure them individually and you’re free to choose from multiple authentication strategies if you’re using more than one.

Defining authentication for a route requires you to provide a config object for the route. Previously, you would set the method, path and handler for a route. Now the handler moves into the config object. To ultimately set the authentication mechanism for a route, put your desired strategy name as a string value to the auth field.

server.route({  
  method: 'GET',
  path: '/private-route',
  config: {
    auth: 'simple',
    handler: (request, h) => {
      return 'Yeah! This message is only available for authenticated users!'
    }
  }
})

As you can see, the route’s config object has the auth field set to the previously registered authentication strategy name simple. Specify the handler function within the config object aside the auth field.

Full Context — Complete Code Snippet

Throughout this tutorial you’ve seen multiple code snippets that ultimately add up to a complete example that build the context of basic authentication with hapi. The following code block composes all the small code chunks that have been used previously to describe the actual functionality.

const Hapi = require('hapi')  
const Bcrypt = require('bcrypt')

const server = new Hapi.Server()

async function liftOff () {  
  await server.register({
    plugin: require('hapi-auth-basic')
  }


  // hardcoded users object … just for illustration purposes
  const users = {
    future: {
      id: '1',
      username: 'future',
      password: '$2a$04$YPy8WdAtWswed8b9MfKixebJkVUhEZxQCrExQaxzhcdR2xMmpSJiG'  // 'studio'
    }
  }

  // validation function used for hapi-auth-basic
  const basicValidation = async function (request, username, password) {
    const user = users[ username ]

    if (!user) {
      return { isValid: false }
    }

    const isValid = await Bcrypt.compare(password, user.password)

    return { isValid, credentials: { id: user.id, username: user.username } }
  }

  server.auth.strategy('simple', 'basic', basicValidation)

  server.route({
    method: 'GET',
    path: '/private-route',
    config: {
      auth: 'simple',
      handler: (request, h) => {
        return 'Yeah! This message is only available for authenticated users!'
      }
    }
  })

  try {
    await server.start()
    console.log('info', 'Server running at: ' + server.info.uri)
  } catch (err) {
    console.error(err)
    process.exit(1)
  }
}

liftOff()  

Running this example code and pointing your browser window to the route http://localhost:3000/private-route will open a sign in a popup that requires you to provide a username and password. Use the combination future/studio to authenticate successfully.

Outlook

Within this tutorial, you’ve learned how to add the functionality for basic authentication to your hapi server instance by adding the hapi-auth-basic plugin and providing the required validation function. Further, you’ve added a route that requires authentication to access.

We appreciate feedback and love to help you if there’s a question in your mind. Let us know in the comments or on twitter @futurestud_io.

Make it rock & enjoy coding!


Additional Resources

Explore the Library

Find interesting tutorials and solutions for your problems.