secure-auth-kit
v2.0.0
Published
Authentication toolkit for Express and MongoDB
Downloads
76
Maintainers
Readme
Secure Auth Kit
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-kitexpress and mongoose are peer dependencies — install whichever versions
your app already uses:
npm install express mongooseQuick 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 required — forgot-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": "..." } } }