· frameworks · 8 min read
Building Robust Authentication with Hapi.js: A Comprehensive Guide
Learn to secure Hapi.js APIs with practical, production-ready authentication patterns: JWT with rotation and revocation, OAuth via Bell, and custom schemes (API keys & HMAC). Includes step-by-step demos, code snippets, and security best practices.

Outcome first: by the end of this article you’ll have three ready-to-adopt authentication strategies for Hapi.js - a secure JWT system with refresh and rotation, an OAuth flow using Bell that links provider accounts to local identities, and a custom scheme (API key/HMAC) for machine-to-machine traffic. Implement these and you’ll cover the common security needs of modern APIs: authentication, authorization, and token lifecycle management.
Why Hapi.js for authentication?
Hapi gives you explicit, composable authentication primitives: schemes (the low-level implementation of how auth works) and strategies (named configurations you apply to routes). That separation makes it straightforward to implement and reason about many patterns - from JWTs to OAuth to custom, signature-based systems - while keeping routes declarative and secure.
Key docs and tools:
- Hapi core docs and auth tutorial: https://hapi.dev/tutorials/auth/ and https://hapi.dev/
- Official JWT plugin: https://github.com/hapijs/jwt
- OAuth via Bell: https://hapi.dev/module/bell/
- JWT best practices: OWASP cheat sheet https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html
Core concepts (quick primer)
- Scheme: low-level implementation. You create a scheme when you need custom auth behavior.
- Strategy: a configured scheme registered under a strategy name, used by routes.
- Validate (or validate function): checks the token/credentials and returns a credentials object (and optionally artifacts) used by routes.
- Scopes / roles: Hapi supports per-route scope checks (
config.auth.scope) to implement RBAC.
Now we’ll walk through three hands-on patterns.
Demo 1 - JWT-based Authentication (access + refresh, rotation-ready)
Outcome: secure, short-lived access tokens (JWT) with refresh tokens implemented safely and with the option to rotate and revoke.
Dependencies (npm):
npm install @hapi/hapi @hapi/jwt jsonwebtoken bcryptjs ioredisWhy Redis? Use a fast datastore to hold refresh tokens / revocation lists and to support token rotation.
Step 1 - register plugin and strategy
const Hapi = require('@hapi/hapi');
const Jwt = require('@hapi/jwt');
const server = Hapi.server({ port: 3000 });
await server.register(Jwt);
server.auth.strategy('jwt', 'jwt', {
keys: {
// prefer RS256 with key pair in production
public: process.env.JWT_PUBLIC_KEY,
private: process.env.JWT_PRIVATE_KEY,
},
verify: {
aud: false,
iss: false,
sub: false,
nbf: true,
exp: true,
maxAgeSec: 300, // 5 minutes access tokens
},
validate: async (artifacts, request, h) => {
// artifacts.decoded.payload is the decoded JWT payload
const { uid, jti } = artifacts.decoded.payload;
// Check token revocation/blacklist in Redis (pseudo)
const revoked = await redis.get(`revoked:${jti}`);
if (revoked) {
return { isValid: false };
}
// Optionally load user and attach roles/scopes
const user = await db.users.findById(uid);
if (!user) return { isValid: false };
return { isValid: true, credentials: { user, scope: user.roles } };
},
});
server.auth.default('jwt');Step 2 - issue tokens (login)
const jwt = require('jsonwebtoken');
const { v4: uuid } = require('uuid');
async function issueTokens(user) {
const jti = uuid();
const accessToken = jwt.sign(
{ uid: user.id, jti },
process.env.JWT_PRIVATE_KEY,
{ algorithm: 'RS256', expiresIn: '5m' }
);
// Create a refresh token (opaque or JWT). Store it server-side.
const refreshToken = uuid();
await redis.set(
`refresh:${refreshToken}`,
JSON.stringify({ uid: user.id }),
'EX',
60 * 60 * 24 * 30
); // 30 days
return { accessToken, refreshToken, jti };
}Notes:
- Use asymmetric keys (RS256) for stronger key management. Keep the private key secure (KMS/secret manager).
- Keep access tokens short-lived (minutes). Use refresh tokens to obtain new access tokens.
- Store refresh tokens server-side (opaque tokens) or store a reference/rotation id. Avoid long-lived JWT refresh tokens unless you can revoke them easily.
Step 3 - token rotation & revocation
Rotation: when a refresh is used, issue a new refresh token and immediately revoke the prior refresh token. This prevents replay attacks.
Revocation: store revoked access token jti values. On logout or suspicious activity, set revoked:{jti} = true with TTL matching token expiry.
Example refresh flow (pseudo):
server.route({
method: 'POST',
path: '/auth/refresh',
options: { auth: false },
handler: async (req, h) => {
const { refreshToken } = req.payload;
const stored = await redis.get(`refresh:${refreshToken}`);
if (!stored) return h.response({ error: 'Invalid refresh' }).code(401);
const data = JSON.parse(stored);
// rotate
await redis.del(`refresh:${refreshToken}`);
const newRefreshToken = uuid();
await redis.set(
`refresh:${newRefreshToken}`,
JSON.stringify({ uid: data.uid }),
'EX',
60 * 60 * 24 * 30
);
// issue new access token
const jti = uuid();
const accessToken = jwt.sign(
{ uid: data.uid, jti },
process.env.JWT_PRIVATE_KEY,
{ algorithm: 'RS256', expiresIn: '5m' }
);
return { accessToken, refreshToken: newRefreshToken };
},
});Testing: use server.inject() for unit tests. Ensure you test revoked tokens, expired access tokens, and refresh rotation cases.
Security tips for JWT:
- Use short access lifetimes and rotate refresh tokens.
- Use HTTPS always.
- Use JTI claim to support revocation / introspection.
- Prefer opaque refresh tokens stored server-side.
- Keep secrets in KMS, not in repo.
References: @hapi/jwt package and OWASP JWT cheat sheet (links above).
Demo 2 - OAuth with Bell (e.g., GitHub sign-in + local account linking)
Outcome: allow users to sign-in with external providers and map them to local identities, then issue your own JWTs for API access.
Install:
npm install @hapi/hapi @hapi/bellRegister Bell and configure GitHub provider (example):
const Bell = require('@hapi/bell');
await server.register(Bell);
server.auth.strategy('github', 'bell', {
provider: 'github',
password: process.env.BELL_COOKIE_PASSWORD, // used to secure the cookie
isSecure: process.env.NODE_ENV === 'production',
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
location: process.env.APP_URL,
});
// route
server.route({
method: ['GET', 'POST'],
path: '/auth/github',
options: { auth: 'github', auth: { mode: 'try' } },
handler: async (req, h) => {
if (!req.auth.isAuthenticated) {
return `Authentication failed: ${req.auth.error.message}`;
}
// req.auth.credentials contains provider profile
const profile = req.auth.credentials.profile;
// Map or create local user
let user = await db.users.findOne({
provider: 'github',
providerId: profile.id,
});
if (!user) {
user = await db.users.create({
provider: 'github',
providerId: profile.id,
email: profile.email,
});
}
// Issue local access token (JWT) for API
const tokens = await issueTokens(user);
return h.response({ user: user.safe(), tokens });
},
});Notes:
- Use server-side session or redirect-based flows per provider documentation. Bell abstracts the OAuth dance for many providers.
- After successful OAuth, create or link a local user record and issue your JWT for consistent authorization across routes.
- For background job access you may want to issue machine tokens or API keys instead of user OAuth flows.
References: Bell docs https://hapi.dev/module/bell/
Demo 3 - Custom Scheme: API key or HMAC-signed requests
Outcome: allow machine-to-machine authentication with a simple scheme (API key) or a stronger HMAC signature for request payloads.
When to use: internal services, webhooks, third-party integrations.
Create a scheme for API Key (header-based) - minimal example:
// register scheme
const apiKeyScheme = function (server, options) {
return {
authenticate: async (request, h) => {
const key = request.headers['x-api-key'];
if (!key) return h.unauthenticated(new Error('Missing API key'));
const record = await db.apiKeys.findOne({ key });
if (!record || record.disabled)
return h.unauthenticated(new Error('Invalid API key'));
const credentials = { id: record.id, service: record.serviceName };
return h.authenticated({ credentials });
},
};
};
server.auth.scheme('apiKey', apiKeyScheme);
server.auth.strategy('apiKeyStrategy', 'apiKey');
// protect a route:
server.route({
method: 'POST',
path: '/internal/data',
options: { auth: 'apiKeyStrategy' },
handler: (req, h) => {
// req.auth.credentials available
return { ok: true };
},
});HMAC-signed requests (stronger):
- Client creates signature: HMAC_SHA256(secret, method + path + timestamp + body)
- Server re-computes HMAC using shared secret and compares in constant time. Reject if timestamp is stale (prevent replay).
Example authenticate for HMAC:
const crypto = require('crypto');
function verifySignature(secret, signature, payload) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(signature, 'hex')
);
}
// inside authenticate:
const sig = req.headers['x-signature'];
const ts = req.headers['x-timestamp'];
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000)
return h.unauthenticated(new Error('Stale request'));
const payload =
req.method + req.path + ts + (req.payload ? JSON.stringify(req.payload) : '');
if (!verifySignature(secret, sig, payload))
return h.unauthenticated(new Error('Invalid signature'));Store per-client secrets, rotate them regularly, and provide revocation endpoints.
Authorization patterns: scopes, roles, and route-level checks
- Use token claims (roles, scopes) to carry authorization context. Keep the claims minimal and authoritative.
- Use Hapi’s
auth: { access: { scope: ['admin'] } }route options to require scope. - For complex rules, create an
onPreHandlerextension that loads user permissions from DB and rejects early.
Example protecting a route by scope:
server.route({
method: 'GET',
path: '/admin',
options: {
auth: {
strategy: 'jwt',
access: { scope: ['admin'] },
},
},
handler: () => ({ adminOnly: true }),
});Operational & Security considerations (do these)
- TLS everywhere. No exceptions.
- Secrets in secure storage (KMS, HashiCorp Vault, cloud secrets manager).
- Rotate keys and secrets - use key identifiers (kid) in JWT header and support multiple active public keys.
- Monitor logins and failed attempts. Alert on anomalous refresh usage or token revocations.
- Rate-limit auth endpoints and protect token endpoints from brute force.
- Use CSP/secure headers for any web UI. For Hapi you can set headers in response lifecycle or use a plugin.
- Limit cookie scope: HttpOnly, Secure, SameSite=Strict where appropriate (for session cookies via OAuth flows).
- Keep dependency versions up to date (especially crypto libraries).
References: OWASP Authentication Cheat Sheet and JWT guidance above.
Testing & debugging tips
- Use Hapi’s
server.inject()for unit tests to emulate requests with different headers/tokens. - Log token jti and user ids for tracing authentication flow (do not log secrets or full JWTs in production logs).
- Use a staging KMS and rotate test keys frequently.
Checklist before production
- Use HTTPS and HSTS.
- Short-lived access tokens; refresh tokens opaque & rotated.
- Revocation mechanism for access and refresh tokens.
- Key management and rotation (kid support for JWTs).
- Rate limiting on auth endpoints.
- Input validation for all auth endpoints.
- Audit & monitoring for suspicious token usage.
Conclusion
Hapi’s explicit auth primitives make it straightforward to design secure, auditable authentication systems. Implement short-lived JWTs with refresh token rotation for user sessions, use Bell for provider-based sign-ins that you map to local accounts, and build custom schemes for machine clients. Prioritize key management, revocation, and monitoring. Do these and your API will be both powerful and resilient.
References
- Hapi.js documentation and auth tutorial: https://hapi.dev/tutorials/auth/
- @hapi/jwt plugin: https://github.com/hapijs/jwt
- Bell (OAuth): https://hapi.dev/module/bell/
- OWASP JSON Web Token Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html



