Hapi.js JWT Authentication Plugin
Hapi-auth-jwt2 is a robust authentication scheme and plugin designed for Hapi.js applications, enabling secure user authentication through JSON Web Tokens (JWTs). The current stable version is 11.0.0. The project demonstrates an active release cadence, with recent version bumps addressing dependency updates, enhancing error handling, and implementing minor breaking changes to align with evolving ecosystem standards like ESLint. Its key differentiators include deep integration within the Hapi framework as a first-party plugin, a flexible `validate` function for custom user authentication logic, and support for advanced features such as 'try mode' and compatibility with multiple authentication strategies. The plugin prioritizes developer-friendliness, offering clear usage examples and comprehensive guidance on both Hapi.js and JWT fundamentals.
Common errors
-
Error: Unknown authentication strategy jwt
cause This error occurs when the 'jwt' authentication strategy is referenced (e.g., in `server.auth.strategy` or `config.auth`) before the `hapi-auth-jwt2` plugin has been successfully registered with the Hapi server.fixEnsure that `await server.register(require('hapi-auth-jwt2'));` is called and successfully awaited before any subsequent calls to `server.auth.strategy('jwt', 'jwt', { ... });` or route definitions that use the 'jwt' strategy. -
Error: Missing Authentication
cause A Hapi route configured with `auth: 'jwt'` was accessed without a JWT being provided in the `Authorization: Bearer <token>` header, or the 'jwt' strategy was not correctly set as the default authentication strategy for the server or route.fixFor restricted routes, ensure a valid JWT is included in the request headers as `Authorization: Bearer <your_token_here>`. Confirm that `server.auth.default('jwt');` is correctly configured or that `config.auth: 'jwt'` is explicitly set for the specific route. -
Error: Invalid token
cause The provided JSON Web Token is malformed, has expired, or was signed using a secret key or algorithm that does not match the configuration provided to the `hapi-auth-jwt2` strategy.fixVerify the JWT string for correct formatting and check its expiration time. Crucially, ensure that the `key` (secret) and `verifyOptions` (e.g., `algorithms`) provided in your `server.auth.strategy` configuration precisely match the parameters used when the token was originally signed. If unsure, generate a new token. -
cb is not a function in hapi-auth-jwt2
cause This error typically indicates an incompatibility between your Hapi.js version and `hapi-auth-jwt2`, or an incorrect signature for your `validate` function. Modern Hapi (v17+) and `hapi-auth-jwt2` expect the `validate` function to be `async` and return an object `{ isValid, credentials }`, rather than accepting and calling a callback.fixUpdate your Hapi.js and `hapi-auth-jwt2` packages to their latest compatible versions. Reconfigure your `validate` function to be an `async` function that returns an object like `{ isValid: boolean, credentials?: object }`, instead of using a callback-based signature.
Warnings
- breaking Version 11.0.0 introduced breaking changes by renaming internal `.js` files to `.cjs` (Common JS) to conform to updated ESLint requirements. This change primarily impacts direct imports of internal plugin files, not the main package entry point.
- breaking Version 10.x.x changed the default structure of artifacts returned on successful authentication, transitioning from a simple String to an Object. This is a breaking change for applications that previously relied on `request.auth.credentials` being a string.
- gotcha Although `jsonwebtoken` is an internal dependency of `hapi-auth-jwt2`, it is strongly recommended to explicitly install it in your project's `package.json` if you plan to sign or generate JWTs within your application. This prevents potential module resolution issues.
- breaking The package now enforces a minimum Node.js engine requirement of version 18.0.0 or higher. Older Node.js versions are no longer supported and will cause installation or runtime failures.
- gotcha Prior to version 5.1.0, `hapi-auth-jwt2` had known compatibility issues when used alongside other authentication strategies, potentially leading to conflicts or unexpected behavior.
- gotcha Earlier versions (before v5.0.3) of the plugin were vulnerable to server crashes if `JWT.decode` encountered malformed or invalid tokens, due to a lack of robust internal error handling. This was addressed by wrapping `JWT.decode` in a try/catch block.
Install
-
npm install hapi-auth-jwt2 -
yarn add hapi-auth-jwt2 -
pnpm add hapi-auth-jwt2
Imports
- hapiAuthJwt2
import hapiAuthJwt2 from 'hapi-auth-jwt2';
const hapiAuthJwt2 = require('hapi-auth-jwt2'); - hapiAuthJwt2 plugin registration
await server.register(hapiAuthJwt2); // If 'hapiAuthJwt2' was imported via ESM 'import' without proper transpilation
await server.register(require('hapi-auth-jwt2')); - StrategyOptions (TypeScript)
import type { StrategyOptions } from 'hapi-auth-jwt2';
Quickstart
const Hapi = require('@hapi/hapi');
const JWT = require('jsonwebtoken'); // Install 'jsonwebtoken' if you intend to sign tokens
const users = { // Our mock "users database"
1: {
id: 1,
name: 'Jen Jones',
scope: ['admin']
}
};
// Define your token validation function
const validate = async function (decoded, request, h) {
// In a real application, perform comprehensive checks (e.g., database lookup)
if (!users[decoded.id]) {
return { isValid: false };
}
// Return isValid: true and provide credentials for request.auth.credentials
return { isValid: true, credentials: users[decoded.id] };
};
const init = async () => {
const server = new Hapi.server({ port: 8000, host: 'localhost' });
// Register the hapi-auth-jwt2 plugin
await server.register(require('hapi-auth-jwt2'));
// Configure the JWT authentication strategy
server.auth.strategy('jwt', 'jwt',
{ key: process.env.JWT_SECRET ?? 'super-secret-jwt-key-replace-me',
validate,
verifyOptions: { algorithms: ['HS256'] } // Specify the algorithm if known
});
// Set 'jwt' as the default authentication strategy for all routes unless overridden
server.auth.default('jwt');
server.route([
{
method: "GET", path: "/", config: { auth: false }, // No authentication required
handler: function(request, h) {
return {text: 'Public route: Token not required. Try POST /login to get a token, then GET /restricted.'};
}
},
{
method: 'GET', path: '/restricted', config: { auth: 'jwt' }, // JWT authentication required
handler: function(request, h) {
const response = h.response({ text: `Hello, ${request.auth.credentials.name}! You used a valid Token!` });
response.header("Authorization", request.headers.authorization);
return response;
}
},
{
method: 'POST', path: '/login', config: { auth: false }, // Public route to obtain a token
handler: function(request, h) {
// In a real app, securely validate username/password from request.payload
const user = users[1]; // Simulate successful login for user with ID 1
const token = JWT.sign({ id: user.id, name: user.name }, process.env.JWT_SECRET ?? 'super-secret-jwt-key-replace-me', { expiresIn: '1h', algorithm: 'HS256' });
return { token };
}
}
]);
await server.start();
console.log('Server running at:', server.info.uri);
console.log('\nTo test:\n1. POST /login to get a JWT.\n2. Copy the token.\n3. GET /restricted with "Authorization: Bearer <token>" header.');
return server;
};
init().catch(err => {
console.error('Error starting server:', err);
process.exit(1);
});