jwt-hash-auth-helper
v1.0.2
Published
Express JWT authentication with bcrypt‑hashed refresh tokens, httpOnly cookies, role‑based middleware, and a plug‑able database interface.
Readme
jwt-hash-auth-helper
JWT authentication helper for Express with hashed refresh tokens, httpOnly cookies, route protection, refresh flow, and optional OTP login support.
What This Package Gives You
- Access token + refresh token flow
- Refresh token hashing with
bcryptjs - Cookie-based login, refresh, and logout handlers
- Middleware for protected routes
- Role-based authorization middleware
- Optional OTP module you can enable only when needed
- Database-agnostic design: Prisma, Mongoose, Sequelize, raw SQL, or custom storage
Install
npm install jwt-hash-auth-helperYou will usually also need:
npm install express cookie-parserRequirements
- Node.js 16+
- Express 4+
cookie-parserif you want cookie-based auth, which is the default and recommended flow
Package Exports
import {
createAuth,
createOtpModule,
errorHandler,
authorize,
ApiError,
catchAsync
} from 'jwt-hash-auth-helper';Most users only need createAuth.
Use createOtpModule only if you want OTP.
Working Flow Overview
Without OTP
- User sends login request.
- Your
validateUser(req)callback verifies credentials and returns the user. - Package creates access and refresh tokens.
- Refresh token is hashed and stored using
saveRefreshToken(user, hashedToken). - Tokens are set in httpOnly cookies.
- Protected routes use
auth.protect. - Refresh route uses
auth.refreshProtectthenauth.refresh. - Logout clears stored refresh token and cookies.
With OTP
- User sends login request.
- Your
validateUser(req)callback verifies credentials and returns the user. - Package generates OTP, hashes it, stores it, and emails it.
- User submits email + OTP to
verifyOtp. - Package verifies OTP and then issues auth cookies.
Quick Start Without OTP
1. Create your auth config
import { createAuth } from 'jwt-hash-auth-helper';
import type { Request } from 'express';
type AppUser = {
id: string;
email: string;
password: string;
role?: string;
refreshToken?: string | null;
};
const users: AppUser[] = [];
const auth = createAuth<AppUser>({
accessSecret: process.env.ACCESS_SECRET!,
refreshSecret: process.env.REFRESH_SECRET!,
accessExpiry: '15m',
refreshExpiry: '7d',
authSource: ['cookie'],
cookie: {
access: 'accessToken',
refresh: 'refreshToken'
},
headers: {
access: 'authorization',
refresh: 'x-refresh-token'
},
secure: false,
sameSite: 'lax',
getUserById: async (id) => {
return users.find((user) => user.id === id) ?? null;
},
validateUser: async (req: Request) => {
const { email, password } = req.body;
const user = users.find((item) => item.email === email);
if (!user) return null;
// Replace this with bcrypt.compare(...) in real apps
if (user.password !== password) return null;
return user;
},
saveRefreshToken: async (user, hashedToken) => {
user.refreshToken = hashedToken;
},
clearRefreshToken: async (user) => {
user.refreshToken = null;
}
});2. Register Express middleware
import express from 'express';
import cookieParser from 'cookie-parser';
import { errorHandler } from 'jwt-hash-auth-helper';
const app = express();
app.use(express.json());
app.use(cookieParser());3. Add auth routes
app.post('/login', auth.login);
app.post('/refresh', auth.refreshProtect, auth.refresh);
app.post('/logout', auth.protect, auth.logout);4. Protect your routes
app.get('/me', auth.protect, (req, res) => {
res.json({
success: true,
user: req.user
});
});5. Add role-based authorization when needed
app.get('/admin', auth.protect, auth.authorize('admin'), (req, res) => {
res.json({ message: 'Admin only route' });
});
app.get('/manager-or-admin', auth.protect, auth.authorize('manager', 'admin'), (req, res) => {
res.json({ message: 'Allowed' });
});6. Add the package error handler
app.use(errorHandler);Complete Minimal Express Example
import express from 'express';
import cookieParser from 'cookie-parser';
import { createAuth, errorHandler } from 'jwt-hash-auth-helper';
import type { Request } from 'express';
type AppUser = {
id: string;
email: string;
password: string;
role?: string;
refreshToken?: string | null;
};
const users: AppUser[] = [
{
id: '1',
email: '[email protected]',
password: '123456',
role: 'admin',
refreshToken: null
}
];
const auth = createAuth<AppUser>({
accessSecret: 'access-secret',
refreshSecret: 'refresh-secret',
secure: false,
sameSite: 'lax',
getUserById: async (id) => users.find((user) => user.id === id) ?? null,
validateUser: async (req: Request) => {
const { email, password } = req.body;
const user = users.find((item) => item.email === email);
if (!user || user.password !== password) return null;
return user;
},
saveRefreshToken: async (user, hashedToken) => {
user.refreshToken = hashedToken;
},
clearRefreshToken: async (user) => {
user.refreshToken = null;
}
});
const app = express();
app.use(express.json());
app.use(cookieParser());
app.post('/login', auth.login);
app.post('/refresh', auth.refreshProtect, auth.refresh);
app.post('/logout', auth.protect, auth.logout);
app.get('/me', auth.protect, (req, res) => {
res.json(req.user);
});
app.use(errorHandler);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});OTP Is Optional
OTP is not required.
If you do not want OTP:
- Do not configure OTP callbacks
- Do not call
createOtpModule(...) - Keep
otp.enabledunset orfalse auth.loginwill work as normal login and directly issue cookies
OTP Setup
Use OTP only when you want email-based OTP verification before issuing tokens.
1. Create the base auth instance
import { createAuth, createOtpModule } from 'jwt-hash-auth-helper';
import type { Request } from 'express';
type AppUser = {
id: string;
email: string;
password: string;
role?: string;
refreshToken?: string | null;
otp?: string;
otpExpiry?: Date;
otpAttempts?: number;
otpLastSentAt?: Date;
};
const authConfig = {
accessSecret: process.env.ACCESS_SECRET!,
refreshSecret: process.env.REFRESH_SECRET!,
secure: false,
sameSite: 'lax',
otp: {
enabled: true,
digits: 6,
expiry: 5,
maxAttempts: 5,
resendAfter: 60,
email: {
service: 'gmail',
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!
}
},
getUserById: async (id: string) => {
return users.find((user) => user.id === id) ?? null;
},
validateUser: async (req: Request) => {
const { email, password } = req.body;
const user = users.find((item) => item.email === email);
if (!user || user.password !== password) return null;
return user;
},
saveRefreshToken: async (user: AppUser, hashedToken: string) => {
user.refreshToken = hashedToken;
},
clearRefreshToken: async (user: AppUser) => {
user.refreshToken = null;
},
getUserByEmail: async (email: string) => {
return users.find((user) => user.email === email) ?? null;
},
saveOtp: async (user: AppUser, data) => {
user.otp = data.otp;
user.otpExpiry = data.otpExpiry;
user.otpAttempts = data.otpAttempts;
user.otpLastSentAt = data.otpLastSentAt;
},
clearOtp: async (user: AppUser) => {
user.otp = undefined;
user.otpExpiry = undefined;
user.otpAttempts = undefined;
user.otpLastSentAt = undefined;
},
updateOtpAttempts: async (user: AppUser, attempts: number) => {
user.otpAttempts = attempts;
}
};
const auth = createAuth<AppUser>(authConfig);
const otpModule = createOtpModule<AppUser>(authConfig);2. Register OTP routes
app.post('/login', auth.login);
app.post('/verify-otp', otpModule.verifyOtp);
app.post('/refresh', auth.refreshProtect, auth.refresh);
app.post('/logout', auth.protect, auth.logout);3. OTP request flow
Login request
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"123456"}'Response on success:
{
"success": true,
"message": "OTP sent successfully",
"data": null
}Verify OTP request
curl -X POST http://localhost:3000/verify-otp \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","otp":"123456"}'Response on success:
{
"success": true,
"message": "Login successful",
"data": null
}At that point, auth cookies are set.
Auth Config Reference
Required options
| Option | Type | Description |
| --- | --- | --- |
| accessSecret | string | Secret for access token signing |
| refreshSecret | string | Secret for refresh token signing |
| getUserById | (id: string) => Promise<T \| null> | Finds user by token payload id |
| validateUser | (req: Request) => Promise<T \| null> | Verifies login request and returns user |
| saveRefreshToken | (user: T, token: string) => Promise<void> | Stores hashed refresh token |
| clearRefreshToken | (user: T) => Promise<void> | Clears stored refresh token on logout |
Optional options
| Option | Type | Default |
| --- | --- | --- |
| accessExpiry | string | '15m' |
| refreshExpiry | string | '7d' |
| authSource | ('cookie' \| 'header')[] | ['cookie'] |
| cookie.access | string | 'accessToken' |
| cookie.refresh | string | 'refreshToken' |
| headers.access | string | 'authorization' |
| headers.refresh | string | 'x-refresh-token' |
| secure | boolean | true |
| sameSite | 'lax' \| 'strict' \| 'none' | 'none' |
| accessMaxAge | number | 15 * 60 * 1000 |
| refreshMaxAge | number | 7 * 24 * 60 * 60 * 1000 |
OTP options
Only needed if you use createOtpModule(...).
| Option | Type | Description |
| --- | --- | --- |
| otp.enabled | boolean | Must be true for OTP mode |
| otp.digits | number | OTP length |
| otp.expiry | number | OTP expiry in minutes |
| otp.maxAttempts | number | Max verification attempts |
| otp.resendAfter | number | Cooldown in seconds |
| otp.email.service | string | Mail service name |
| otp.email.user | string | SMTP/email username |
| otp.email.pass | string | SMTP/email password |
| otp.email.host | string | Optional custom SMTP host |
| otp.email.port | number | Optional custom SMTP port |
| otp.email.secure | boolean | Optional SMTP secure flag |
| otp.template | (otp, user) => { subject, html } | Custom email template |
| getUserByEmail | (email: string) => Promise<T \| null> | Required for OTP verification |
| saveOtp | (user, data) => Promise<void> | Stores hashed OTP and OTP metadata |
| clearOtp | (user) => Promise<void> | Clears OTP after success or lockout |
| updateOtpAttempts | (user, attempts) => Promise<void> | Tracks failed OTP attempts |
Route Summary
Core auth routes
app.post('/login', auth.login);
app.post('/refresh', auth.refreshProtect, auth.refresh);
app.post('/logout', auth.protect, auth.logout);OTP routes
app.post('/login', auth.login);
app.post('/verify-otp', otpModule.verifyOtp);Request Notes
validateUser(req)
This callback is responsible for your actual login logic. Typical work inside it:
- read
req.body.email - read
req.body.password - check the database
- compare hashed password
- return the user if valid
- return
nullif invalid
getUserById(id)
This callback is used by:
auth.protectauth.refreshProtect
It must return the same user shape your application stores.
Cookie and Header Behavior
Cookies
The package writes auth tokens to cookies during:
auth.loginauth.refreshotpModule.verifyOtp
Headers
The middleware can read tokens from headers if you set:
authSource: ['header']or:
authSource: ['cookie', 'header']Headers used:
Authorization: Bearer <access-token>
x-refresh-token: <refresh-token>Important: the current package issues tokens through cookies. Header mode is useful for reading incoming tokens on protected and refresh requests.
Response Format
Success
{
"success": true,
"message": "Login successful",
"data": null
}Error
{
"success": false,
"error": {
"code": "AUTH_INVALID_TOKEN",
"message": "Invalid token"
}
}Testing With cURL
Login without OTP
curl -i -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"123456"}'Access protected route using cookies
curl -i http://localhost:3000/me \
--cookie "accessToken=YOUR_ACCESS_TOKEN"Refresh token
curl -i -X POST http://localhost:3000/refresh \
--cookie "refreshToken=YOUR_REFRESH_TOKEN"Logout
curl -i -X POST http://localhost:3000/logout \
--cookie "accessToken=YOUR_ACCESS_TOKEN"Production Notes
- Use strong secrets from environment variables
- Set
secure: truein production HTTPS environments - Use real password hashing such as
bcrypt.compare()insidevalidateUser - Store only hashed refresh tokens in your database
- Add rate limiting on login, refresh, and OTP routes
- For browsers on different domains, configure CORS and cookie settings properly
License
MIT � Shahnawaz Siddiqui
