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
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
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:
name
: your strategy name that will be used throughout your appscheme
: the scheme name on which the strategy is based upon (e.g.basic
)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 databaseallowEmptyUsername
: boolean value that indicates whether users are allowed to make requests without a username; defaults tofalse
unauthorizedAttributes
: object that will be passed toBoom.unauthorized
if set. If there’s a custom error defined, this object will be ignored in favor of the custom error data; defaults toundefined
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 authenticationusername
: username send by the clientpassword
: password send by the client
The return valie for validateFunc
is an object, containing the following fields:
err
: internal error object that replaces the defaultBoom.unauthorized
if definedisValid
: boolean that indicates if the username was found and passwords matchcredentials
: user’s credentials, only included ifisValid
istrue
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
- hapi-basic-auth plugin on GitHub
- Our NPM Quick Tips — #2 Use Shortcuts to Install Packages
- Bcrypt for Node.js