npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

npm version License: MIT npm downloads

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-helper

You will usually also need:

npm install express cookie-parser

Requirements

  • Node.js 16+
  • Express 4+
  • cookie-parser if 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

  1. User sends login request.
  2. Your validateUser(req) callback verifies credentials and returns the user.
  3. Package creates access and refresh tokens.
  4. Refresh token is hashed and stored using saveRefreshToken(user, hashedToken).
  5. Tokens are set in httpOnly cookies.
  6. Protected routes use auth.protect.
  7. Refresh route uses auth.refreshProtect then auth.refresh.
  8. Logout clears stored refresh token and cookies.

With OTP

  1. User sends login request.
  2. Your validateUser(req) callback verifies credentials and returns the user.
  3. Package generates OTP, hashes it, stores it, and emails it.
  4. User submits email + OTP to verifyOtp.
  5. 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.enabled unset or false
  • auth.login will 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 null if invalid

getUserById(id)

This callback is used by:

  • auth.protect
  • auth.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.login
  • auth.refresh
  • otpModule.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: true in production HTTPS environments
  • Use real password hashing such as bcrypt.compare() inside validateUser
  • 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