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

secure-auth-kit

v2.0.0

Published

Authentication toolkit for Express and MongoDB

Downloads

76

Readme

Secure Auth Kit

npm license

Authentication toolkit for Express.js and MongoDB — register, email/OTP verification, login, password reset, token refresh, and role-based access control, wired onto your own Mongoose schema and Express app.


Installation

npm install secure-auth-kit

express and mongoose are peer dependencies — install whichever versions your app already uses:

npm install express mongoose

Quick Start

// models/User.ts
import { Schema, model } from 'mongoose';
import { userPlugin } from 'secure-auth-kit';

const userSchema = new Schema({
    name: { type: String, required: true }, // your own fields stay untouched
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
});

userSchema.plugin(userPlugin); // adds the fields secure-auth-kit needs

export const User = model('User', userSchema);
// server.ts
import express from 'express';
import mongoose from 'mongoose';
import { secureAuth } from 'secure-auth-kit';
import { User } from './models/User.js';
import { sendEmail } from './email.js'; // your own nodemailer/Resend/SES setup

const app = express();
app.use(express.json());

await mongoose.connect(process.env.MONGO_URI!);

secureAuth(app, {
    userModel: User,
    jwt: {
        accessSecret: 'your_jwt_access_secret',
        refreshSecret: 'your_jwt_refresh_secret',
        accessExpiry: '15m', // default
        refreshExpiry: '7d', // default
    },
    sendEmail: async ({ to, subject, otp, resetLink, resetToken }) => {
        await sendEmail({
            to,
            subject,
            text: otp ? `Your OTP is ${otp}` : `Reset here: ${resetLink}`,
        });
    },
});

app.listen(3000);

sendEmail is requiredforgot-password always needs it to deliver reset links, and register needs it too whenever enableEmailVerification is on. secure-auth-kit never bundles or calls an email provider itself; you own the transport (nodemailer, Resend, SES, whatever you already use).

This registers the following routes under /auth (configurable via routePrefix):

| Method | Route | Auth required | Notes | | ------ | --------------------------- | ------------- | ----------------------------------------------------- | | POST | /auth/register | No | Skips OTP step unless enableEmailVerification is on | | POST | /auth/verify-otp | No | Only relevant if enableEmailVerification is on | | POST | /auth/login | No | | | POST | /auth/forgot-password | No | Always responds the same way, account or not | | POST | /auth/reset-password/:token | No | | | POST | /auth/refresh-token | No | Reads the refresh_token httpOnly cookie | | GET | /auth/me | Yes | |

Every route above has its own built-in rate limit (not currently configurable): register/login/verify-otp/reset-password allow 5 attempts per 15 minutes, forgot-password allows 3, refresh-token allows 20 — each on its own bucket, so hitting one limit never blocks the others.


User Model Requirements

Your Mongoose schema must have email and password. Everything else secure-auth-kit needs — passwordResetToken, passwordResetTokenExpires, and conditionally otp/otpExpires/isEmailVerified/role — is added for you by userPlugin. All of this is validated at startup; secureAuth() throws a descriptive error immediately if anything required is missing, rather than failing confusingly mid-request later.

import { Schema, model } from 'mongoose';
import { userPlugin } from 'secure-auth-kit';

const ROLES = ['user', 'admin', 'moderator'];

const userSchema = new Schema({
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
});

userSchema.plugin(userPlugin, {
    enableEmailVerification: true,
    enableRBAC: true,
    roles: ROLES,
});

export const User = model('User', userSchema);

Passwords are hashed automatically on register and reset, and compared on login — never hash or compare them yourself.


Configuration

secureAuth(app, {
    userModel: User,

    routePrefix: '/auth', // optional, default: "/auth"

    enableEmailVerification: true, // optional, default: false — gates the OTP step on register
    enableRBAC: true, // optional, default: false — gates role-based routes
    roles: ['user', 'admin', 'moderator'], // optional, default: ["user", "admin"]
    // MUST match the `roles` array you gave userPlugin

    jwt: {
        accessSecret: process.env.JWT_ACCESS_SECRET!,
        refreshSecret: process.env.JWT_REFRESH_SECRET!,
        accessExpiry: '15m', // optional, default: "15m"
        refreshExpiry: '7d', // optional, default: "7d"
    },

    otp: {
        length: 6, // optional, default: 6
        expiryMinutes: 10, // optional, default: 10
    },

    passwordResetUrl: 'https://myapp.com/reset-password', // optional — if set, sendEmail
    // receives a full resetLink;
    // otherwise just the raw resetToken

    sendEmail: async ({ to, subject, otp, resetLink, resetToken }) => {
        // required — wire up your own email provider here
    },
});

Protecting Your Own Routes

authenticate — verify the request is from a logged-in user

import { authenticate } from 'secure-auth-kit';

app.get('/protected', authenticate, (req, res) => {
    res.json({ user: req.user });
});

requireRole — restrict a route to specific roles

Requires enableRBAC: true and a matching roles array in both secureAuth() and userPlugin(). Always goes after authenticate.

import { authenticate, requireRole } from 'secure-auth-kit';

app.get('/admin/dashboard', authenticate, requireRole('admin'), adminHandler);

app.delete(
    '/posts/:id',
    authenticate,
    requireRole('admin', 'moderator'), // accepts multiple roles
    deletePostHandler
);

If enableRBAC isn't on, or a role you pass doesn't exist in your configured roles, requireRole fails loudly with a clear message in your server logs (and a generic 500 to the caller — your config mistakes are never leaked to whoever hit the route).


API Reference

All responses share one of two shapes:

{
    "success": true,
    "data": {
        /* ... */
    },
    "message": "optional",
    "statusCode": 200
}
{
    "success": false,
    "error": {
        "message": "...",
        "code": "SOME_CODE",
        "statusCode": 400
    }
}

POST /auth/register

{
    "email": "[email protected]",
    "password": "Secret@123"
}

If enableEmailVerification is off, returns immediately with tokens:

{
    "success": true,
    "message": "Registration successful",
    "data": {
        "user": { "id": "...", "email": "[email protected]", ... },
        "accessToken": "..."
    }
}

If enableEmailVerification is on, no tokens are issued yet:

{
    "success": true,
    "message": "Registered. Check your email for the OTP to verify your account.",
    "data": { "userId": "...", "email": "[email protected]" }
}

POST /auth/verify-otp

(only relevant if enableEmailVerification is on)

{ "email": "[email protected]", "otp": "123456" }

Returns the same { user, accessToken } shape as register, and sets the refresh cookie.

POST /auth/login

{ "email": "[email protected]", "password": "Secret@123" }

Returns the same { user, accessToken } shape as register.

POST /auth/forgot-password

{ "email": "[email protected]" }

Always returns the same response, whether or not that email has an account — this is deliberate, so the endpoint can't be used to discover which emails are registered:

{
    "success": true,
    "message": "If an account exists for that email, a reset link has been sent.",
    "data": null
}

POST /auth/reset-password/:token

{ "password": "NewSecret@123" }
{ "success": true, "message": "Password updated successfully", "data": null }

POST /auth/refresh-token

No request body — the refresh token is read from the refresh_token httpOnly cookie automatically. A new refresh token is issued and rotated into the cookie on every call; the old one stops working.

{ "success": true, "message": "Token refreshed", "data": { "accessToken": "..." } }

GET /auth/me (requires Authorization: Bearer <accessToken>)

{ "success": true, "data": { "user": { "id": "...", "email": "..." } } }