Project Overview
light-auth is a lightweight, flexible authentication module for Express.js applications. It provides a ready-made solution for common auth needs while offering deep customization through hooks and configuration options.
What is light-auth?
light-auth initializes all authentication workflows via a single setupAuth
function. It ships with:
- JWT and session-based login
- User registration and email verification
- Password reset flows
- Role-based access control (RBAC)
- Default REST routes for all auth actions
- Customizable middleware and lifecycle hooks
Why light-auth?
Express projects often reimplement:
- Token/session management
- Email flows (verification, reset)
- Guarded routes by role
- Custom hooks for business logic
light-auth centralizes these concerns, reduces boilerplate, and scales from simple apps to complex systems.
Core Features
- Dual Strategies: JWT or cookie-based sessions
- User Management: Registration, login, logout
- Email Workflows: Verification and password reset
- Access Control: Define roles and protect routes
- Lifecycle Hooks: Customize database queries, email templates, token lifetimes
- Default Routes:
/register
,/login
,/logout
,/verify-email
,/reset-password
- Flexible Config: Override any default, integrate with ORMs or custom stores
Quick Start
Install package:
npm install light-auth
Initialize in your Express app:
const express = require('express');
const { setupAuth } = require('light-auth');
const app = express();
app.use(express.json());
setupAuth(app, {
jwtSecret: process.env.JWT_SECRET,
sessionSecret: process.env.SESSION_SECRET,
db: {
// e.g., Sequelize or custom connectors
client: require('./dbClient'),
},
email: {
transporter: require('./emailTransporter'),
from: 'no-reply@yourapp.com'
},
roles: ['user', 'admin']
});
// Your protected route
app.get('/admin', app.auth.requireRole('admin'), (req, res) => {
res.send('Welcome, Admin!');
});
app.listen(3000, () => console.log('Server running on port 3000'));
Advanced Usage
Override default hooks to inject business logic:
setupAuth(app, {
// ...other config
hooks: {
// Modify user payload before registration
beforeRegister: async (userData) => {
userData.profileCreatedAt = new Date();
return userData;
},
// Log every successful login
onLoginSuccess: async ({ user }) => {
await logAuthEvent({ userId: user.id, event: 'login' });
}
}
});
With its minimal setup and rich feature set, light-auth accelerates building secure, production-ready Express applications.
Quick Start
Get up and running with user registration, login and logout in minutes.
1. Install
Install the package and peer dependencies:
npm install light-auth express
2. Minimal Express App
Create server.js
:
const express = require('express');
const { setupAuth, auth } = require('light-auth');
const app = express();
// Parse JSON bodies
app.use(express.json());
// Initialize authentication
// - session-based by default
// - default routes under /auth
setupAuth(app, {
// replace with a strong secret in production
sessionSecret: 'your-session-secret',
// optional: change route prefix (default: '/auth')
// routePrefix: '/api/auth',
});
app.get('/', (req, res) => {
res.send('Welcome to Light Auth Quick Start');
});
// Protected route example
app.get('/profile', auth.required, (req, res) => {
// req.user is set after successful login
res.json({ email: req.user.email, id: req.user.id });
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
});
3. Register a User
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"P@ssw0rd"}'
Successful response returns HTTP 201 and a JSON payload:
{
"id": "605c2b9f4f1a2c0015e8a9d3",
"email": "user@example.com"
}
4. Log In
curl -X POST http://localhost:3000/auth/login \
-c cookies.txt \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"P@ssw0rd"}'
- Session cookie is stored in
cookies.txt
. - Response HTTP 200.
5. Access Protected Route
curl http://localhost:3000/profile \
-b cookies.txt
Response HTTP 200 returns the user’s profile:
{
"email": "user@example.com",
"id": "605c2b9f4f1a2c0015e8a9d3"
}
6. Log Out
curl -X POST http://localhost:3000/auth/logout \
-b cookies.txt
- Server clears session.
- Subsequent access to
/profile
returns HTTP 401.
You now have registration, login, logout and protected routes running with Light Auth. For advanced configuration (JWT, email verification, hooks), see the full documentation.
Core Concepts & Configuration
This section covers the foundational modules you must understand before configuring Light-Auth: session management, one-time passwords, request authentication, and password policy validation. Each subsection explains key concepts, exposes configuration points, and provides practical code examples.
Configuring Session Middleware with setupSession
Initialize and secure Express session handling with sensible defaults, environment-aware settings, and optional custom stores.
The setupSession
helper wraps express-session configuration, enforcing:
- A strong session secret (≥16 chars)
- Explicit
resave
andsaveUninitialized
flags - Secure cookie defaults (
httpOnly
,sameSite: 'lax'
,secure
in production) - Optional session store injection (e.g., Redis)
API
import { setupSession } from "./modules/auth/session.js";
/**
* @param {import('express').Application} app
* @param {string} secret // ≥16-character string
* @param {object} sessionConfig
* @param {boolean} sessionConfig.resave
* @param {boolean} sessionConfig.saveUninitialized
* @param {object} sessionConfig.cookie
* @param {boolean} [sessionConfig.cookie.secure]
* @param {import('express-session').Store} [sessionConfig.store]
*/
setupSession(app, secret, sessionConfig);
Key Behaviors
- Secret Validation
Throws if!secret || typeof secret!=="string" || secret.length<16
. - Flag Enforcement
Requires booleanresave
andsaveUninitialized
. - Environment Awareness
InNODE_ENV==="production"
, setscookie.secure = true
unless explicitlyfalse
. - Cookie Defaults
Merges user-suppliedcookie
with:httpOnly: true
sameSite: "lax"
secure
: as above
- Custom Store
Accepts any compatible store (e.g.,connect-redis
). Falls back to in-memory (not for production).
Example: Basic Setup
import express from "express";
import RedisStore from "connect-redis";
import session from "express-session";
import { setupSession } from "./modules/auth/session.js";
const app = express();
const redisClient = /* initialized ioredis or node-redis client */;
const store = new RedisStore({ client: redisClient });
setupSession(app, process.env.SESSION_SECRET, {
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24, // 1 day
secure: true // override only if not in production
},
store
});
Practical Guidance
- Always supply a strong, unique secret (
process.env.SESSION_SECRET
≥16 chars). - Set
saveUninitialized: false
to avoid unnecessary sessions. - In production:
- Ensure
cookie.secure
istrue
(HTTPS only). - Use a persistent store (Redis, MongoDB) to avoid memory leaks.
- Ensure
- Customize
cookie.maxAge
as needed; defaults to session-only. - Call
setupSession
once per app instance; repeated calls throw or override existing middleware.
OTP Service: Generating and Validating One-Time Passwords
Secure, configurable functions to generate OTPs and verify them safely (timing-safe comparison and expiry checks).
API
import { generateOTP, isOTPValid } from "./modules/email/services/otpService.js";
/**
* generateOTP(length = 6, type = "numeric"): string
* - length: number of characters in the OTP
* - type: "numeric" | "alphanumeric"
*/
const otp = generateOTP();
/**
* isOTPValid(storedOtp, expiry, providedOtp): boolean
* - storedOtp: string
* - expiry: Date | number | ISO string
* - providedOtp: string
*/
const valid = isOTPValid(storedOtp, expiry, providedOtp);
Examples
// 1. Generate a 6-digit numeric OTP
const otp1 = generateOTP(); // e.g. "527394"
// 2. Generate an 8-character alphanumeric OTP
const otp2 = generateOTP(8, "alphanumeric"); // e.g. "A4b9Z1qL"
// 3. Store OTP and expiry in user document
user.emailOtp = otp1;
user.emailOtpExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
await user.save();
// 4. Later, validate incoming OTP
if (!isOTPValid(user.emailOtp, user.emailOtpExpires, req.body.otp)) {
return res.status(400).json({ error: "Invalid or expired OTP." });
}
// proceed to verify user or reset password
Practical Tips
- Always pair
generateOTP
with an expiry timestamp stored alongside (Date.now() + minutes*60000
). - Use
isOTPValid
to reject missing or expired OTPs and prevent timing attacks viacrypto.timingSafeEqual
. - Choose
length
andtype
based on security needs:- Numeric: simpler input (SMS/email)
- Alphanumeric: higher entropy
- Integrate in Express routes to centralize OTP logic.
router.post("/verify-otp", async (req, res) => {
const { email, otp } = req.body;
const user = await UserModel.findOne({ email });
if (!user || !isOTPValid(user.otp, user.otpExpires, otp)) {
return res.status(400).json({ error: "Invalid or expired OTP." });
}
res.json({ message: "OTP verified." });
});
authenticate(useSession, jwtSecret)
Middleware
Verify incoming requests via session or JWT, attach a unified req.user
payload on success, and return 401 on failure.
Behavior
- If
useSession
istrue
, checksreq.session.user
; else returns 401. - If
useSession
isfalse
, expectsAuthorization: Bearer <token>
header:- Verifies token with
jwtSecret
. - Ensures decoded payload contains
id
androle
.
- Verifies token with
- On success, sets:
req.user = { id: decoded.id, role: decoded.role, ...decoded };
- Catches JWT errors, distinguishing expired tokens (
TokenExpiredError
) from other invalid tokens.
Usage
import express from "express";
import { authenticate } from "./modules/middleware/authMiddleware.js";
const app = express();
const JWT_SECRET = process.env.JWT_SECRET;
// JWT-based protection
app.get(
"/api/profile",
authenticate(false, JWT_SECRET),
(req, res) => {
// req.user → { id, role, ... }
res.json({ profile: /* fetch by req.user.id */ });
}
);
// Session-based protection
app.get(
"/dashboard",
authenticate(true, JWT_SECRET),
(req, res) => {
// req.user comes from req.session.user
res.render("dashboard", { user: req.user });
}
);
Practical Tips
- Mount body parsers and
setupSession
before this middleware. - For JWT flows, ensure clients handle 401 by refreshing tokens or re-authenticating.
- Combine with
authorize(roles)
downstream for role-based access control.
Password Policy Validator
Generate a reusable password-validation function based on your security requirements.
API
import { validatePassword, defaultPasswordPolicy }
from "./modules/utils/validators.js";
/**
* validatePassword(policy?): (password: string) => string|null
* - policy.minLength?: number
* - policy.requireUppercase?: boolean
* - policy.requireLowercase?: boolean
* - policy.requireNumbers?: boolean
* - policy.requireSymbols?: boolean
*/
const validator = validatePassword({
minLength: 10,
requireNumbers: true,
requireSymbols: true
});
Default policy:
// defaultPasswordPolicy === {
// minLength: 8,
// requireUppercase: false,
// requireLowercase: false,
// requireNumbers: false,
// requireSymbols: false
// }
Example: Direct Express Use
import express from "express";
import { validatePassword } from "./modules/utils/validators.js";
const app = express();
app.use(express.json());
const passwordValidator = validatePassword({
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true
});
app.post("/change-password", (req, res) => {
const { newPassword } = req.body;
const error = passwordValidator(newPassword);
if (error) return res.status(400).json({ error });
// proceed to hash and update password...
res.sendStatus(204);
});
Integration in setupAuth
import express from "express";
import { setupAuth } from "./setupAuth.js";
const app = express();
await setupAuth(app, {
jwtSecret: process.env.JWT_SECRET,
db: mongooseConnection,
passwordPolicy: {
minLength: 12,
requireUppercase: true,
requireNumbers: true
}
});
setupAuth
deep-merges your passwordPolicy
with defaults and applies the validator in registration and password-change routes.
Practical Guidance
- Enforce at least one lowercase, uppercase, number, and symbol in production.
- Set
minLength
per organizational policy (e.g., 12+ chars). - Validate before hashing or DB writes to avoid wasted work.
- Reuse the same validator across all password endpoints for consistency.
Routes & Middleware
This section documents the Express routes and middleware exposed by light-auth after initialization. You’ll find:
- A registration endpoint (
registerRoute
) - Email-based OTP routes (
setupEmailRoutes
) - Authentication and authorization middleware (
authenticate
,authorize
)
User Registration Route (registerRoute
)
Purpose: Mounts a POST /register
endpoint with rate limiting, input validation, password hashing, role enforcement and optional lifecycle hooks.
Function Signature
registerRoute(router, UserModel, roles, validatePassword, rateLimitOptions, config = {})
Parameters
- router: Express Router instance
- UserModel: Mongoose-style model with
.findOne()
and.create()
- roles: Array of allowed role strings (e.g.
['user','admin']
) - validatePassword: fn(password) →
null
for valid or error message string - rateLimitOptions: Options for
express-rate-limit
(e.g.{ windowMs:60000, max:5 }
) - config.hooks (optional):
onRegister(user)
: called after successful creationonError({ type, error, req })
: called on any registration error
Behavior
- Applies rate limiting
- Validates
email
format viavalidateEmail
- Validates
password
viavalidatePassword
- Ensures
role
is inroles
(defaults to"user"
) - Returns
409 Conflict
if user exists - Hashes password with bcrypt (12 rounds)
- Creates user, responds with
{ _id, email, role }
- Invokes hooks on success or error
Code Example
import express from 'express'
import mongoose from 'mongoose'
import { registerRoute } from './modules/routes/authRoutes.js'
import { validatePassword } from './utils/passwordPolicy.js'
import UserModel from './models/User.js'
const app = express()
const router = express.Router()
const roles = ['user', 'admin']
const rateLimitOptions = { windowMs: 60_000, max: 10 }
const config = {
hooks: {
onRegister: async user => {
// e.g. send welcome email
await sendWelcomeEmail(user.email)
},
onError: async ({ type, error }) => {
console.error(`Error in ${type}`, error)
}
}
}
app.use(express.json())
registerRoute(router, UserModel, roles, validatePassword, rateLimitOptions, config)
app.use('/auth', router)
app.listen(3000)
cURL Example
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"Str0ngP@ss!","role":"user"}'
Practical Tips
- Customize
validatePassword
for your policy; return string ornull
. - Tune
rateLimitOptions
per traffic. - Use
onRegister
to trigger side-effects. - Handle errors in
onError
without leaking internals.
Email Routes Setup (setupEmailRoutes
)
Purpose: Mount email OTP endpoints for verification and password reset with minimal configuration.
Function Signature
setupEmailRoutes(app, UserModel, config)
- app: Express application instance
- UserModel: Mongoose-style model with fields
email
,password
,emailOtp
,emailOtpExpires
,resetOtp
,resetOtpExpires
,verified
- config: Controls routes, toggles, OTP params and hooks
Configuration Shape
const emailConfig = {
route: "/auth", // Base path
emailVerification: {
enabled: true,
otpLength: 6,
otpType: "numeric", // "numeric" | "alphanumeric"
otpExpiryMinutes: 10,
sendMail: async ({ email, otp, type, url }) => { /* your mailer */ },
url: "https://example.com/verify"
},
forgotPassword: {
enabled: true,
otpLength: 8,
otpType: "alphanumeric",
otpExpiryMinutes: 15,
sendMail: async ({ email, otp, type, url }) => { /* your mailer */ },
url: "https://example.com/reset"
},
hooks: {
onVerify: async user => { /* post-verification logic */ }
}
}
Initialization Example
import express from "express"
import mongoose from "mongoose"
import { setupEmailRoutes } from "./modules/email/routes/emailRoutes.js"
import UserModel from "./models/User.js"
const app = express()
app.use(express.json())
setupEmailRoutes(app, UserModel, emailConfig)
app.listen(3000, () => console.log("Server running on port 3000"))
Exposed Endpoints (at {route}
)
- POST
/send-verification-otp
- Body:
{ email }
- Sends OTP via
sendMail
- Response:
{ message: "Verification OTP sent." }
- Body:
- POST
/verify-email
- Body:
{ email, otp }
- Marks
user.verified = true
, clears OTP, callshooks.onVerify
- Response:
{ message: "Email verified successfully." }
- Body:
- POST
/send-forgot-otp
- Body:
{ email }
- Sends reset OTP
- Response:
{ message: "Password reset OTP sent." }
- Body:
- POST
/reset-password
- Body:
{ email, otp, newPassword }
- Hashes new password, updates
user.password
, clears OTP - Response:
{ message: "Password reset successful." }
- Body:
Practical Tips
- Extend
UserModel
schema withemailOtp
,resetOtp
, expiry fields, andverified
boolean. - Use
express-validator
for payload checks via bundled validators. - Integrate
sendMail
with your SMTP or transactional email service. - Adjust
otpLength
,otpType
, andotpExpiryMinutes
per security policy.
Securing Routes with authenticate
and authorize
Purpose: Protect routes by verifying identity (session or JWT) and enforcing role-based access.
1. Obtain Middleware from setupAuth
import express from "express"
import mongoose from "mongoose"
import { setupAuth } from "./setupAuth.js"
async function init() {
const app = express()
const config = {
db: mongooseConnection,
jwtSecret: process.env.JWT_SECRET,
useSession: false,
roles: ["user", "admin"]
}
const { auth } = await setupAuth(app, config)
// auth.authenticate: verifies session or JWT, sets req.user
// auth.authorize: enforces req.user.role
}
2. Authenticate Only
app.get(
"/profile",
auth.authenticate, // 401 if unauthenticated
(req, res) => {
res.json({ id: req.user.id, role: req.user.role })
}
)
3. Authenticate + Authorize
// Only admins can delete users
app.delete(
"/users/:id",
auth.authenticate,
auth.authorize(["admin"]), // 403 if not admin
async (req, res) => {
await UserModel.deleteOne({ _id: req.params.id })
res.json({ success: true })
}
)
Common Pitfalls & Tips
- Call
setupAuth
before defining protected routes to register session/JWT middleware. - Always chain
authenticate
beforeauthorize
. - Pass multiple roles for mixed-role access:
authorize(["admin","manager"])
. - Wrap middleware in custom error handlers for bespoke response formats.
API Reference
setupAuth(app, config)
Initializes authentication routes, sessions, JWT settings, rate limiting, and email workflows on an Express application.
Signature
/**
* @param {import('express').Application} app
* @param {Object} config
* @returns {Promise<{
* auth: { authenticate: Function, authorize: Function },
* models: { User: any }
* }>}
*/
await setupAuth(app, config)
Key Configuration Options
- route (string): Base path for auth routes (default
"/auth"
). - useSession (boolean): Enable express-session alongside JWT (default
false
). - roles (string[]): Roles to seed into the User model (default
["user"]
). - passwordPolicy (object):
minLength
(number, default 8)
- rateLimiting (object):
login
/register
:{ windowMs, max, message, store }
- sessionConfig (object): Options for
express-session
(cookie, store, etc.) - jwtConfig (object): JWT signing options (
expiresIn
, etc.) - jwtSecret (string, ≥16 chars): Secret for signing tokens
- db: Mongoose connection (
.model()
must be available) - User: Optional pre-built Mongoose model
- emailVerification / forgotPassword (objects):
enabled
(boolean)requiredToLogin
(boolean)sendMail
(async function)
Behavior
- Deep-merges
config
over defaults - Validates presence of
jwtSecret
anddb
- Mounts:
- Helmet security headers
- Optional session middleware
- JSON body parser on auth router
- POST
/auth/register
,/auth/login
,/auth/logout
(with rate limits) - Email routes if enabled (
/auth/verify
,/auth/forgot-password
)
- Returns
{ auth: { authenticate, authorize }, models: { User } }
Example
import express from "express"
import mongoose from "mongoose"
import { setupAuth } from "./setupAuth.js"
async function bootstrap() {
const app = express()
await mongoose.connect(process.env.MONGO_URI)
const { auth, models } = await setupAuth(app, {
db: mongoose,
jwtSecret: process.env.JWT_SECRET,
useSession: true,
passwordPolicy: { minLength: 10 },
rateLimiting: {
login: { max: 10, windowMs: 600_000 }
},
emailVerification: {
enabled: true,
requiredToLogin: true,
sendMail: async ({ to, subject, html }) => {
// integrate your mailer here
}
}
})
app.get("/profile", auth.authenticate, (req, res) => {
res.json({ user: req.user })
})
app.listen(3000)
}
bootstrap()
Tips
- Use a strong
JWT_SECRET
(≥16 random chars). - For sessions, configure a production-grade
sessionConfig.store
. - Provide a real
sendMail
implementation or enableALLOW_MOCK_EMAILS
. - Protect role-based routes with
auth.authorize("admin", "user")
.
validatePassword(policy?)
Provides a policy-driven password validator that returns an error message or null
if valid.
Signature
function validatePassword(
policy?: {
minLength?: number
requireUppercase?: boolean
requireLowercase?: boolean
requireNumbers?: boolean
requireSymbols?: boolean
}
): (password: string) => string | null
const defaultPasswordPolicy = {
minLength: 8,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSymbols: false
}
Usage
import { validatePassword, defaultPasswordPolicy } from "modules/utils/validators"
// Default policy
const checkDefault = validatePassword()
// Strong policy
const strongPolicy = {
minLength: 12,
requireUppercase: true,
requireNumbers: true,
requireSymbols: true
}
const checkStrong = validatePassword(strongPolicy)
console.log(checkDefault("short")) // "Password must be at least 8 characters long."
console.log(checkStrong("LongerPass1!")) // null
console.log(checkStrong("NoNumbers!")) // "Password must contain at least one number."
Policy Options
- minLength (number, default 8): Minimum total characters
- requireUppercase (boolean): Require ≥1 uppercase letter
- requireLowercase (boolean): Require ≥1 lowercase letter
- requireNumbers (boolean): Require ≥1 digit
- requireSymbols (boolean): Require ≥1 symbol (
!@#$%
, etc.)
Integration
- Run in registration or password-reset handlers
- Display returned string directly as user feedback
- Combine with
validateEmail
for full credential checks
generateOTP(length?, type?)
Creates a secure one-time password (OTP).
Signature
function generateOTP(
length?: number, // default 6
type?: "numeric" | "alphanumeric" // default "numeric"
): string
Example
import { generateOTP } from "modules/email/services/otpService"
// 6-digit numeric
const otp1 = generateOTP() // e.g. "483920"
// 8-character alphanumeric
const otp2 = generateOTP(8, "alphanumeric") // e.g. "A9bZ3kLp"
Tips
- Use numeric for ease of entry on mobile
- Use alphanumeric + longer length (≥8) for higher entropy
- Store with an expiration timestamp in your database
isOTPValid(storedOtp, expiry, providedOtp)
Validates an OTP against its stored value and expiry using a timing-safe comparison.
Signature
function isOTPValid(
storedOtp: string,
expiry: Date | number | string,
providedOtp: string
): boolean
Example Workflow
import { generateOTP, isOTPValid } from "modules/email/services/otpService"
// 1. Issue OTP
const otp = generateOTP()
const expiresAt = Date.now() + 5 * 60_000
await db.insert("otps", { userId, otp, expiresAt })
// 2. Validate later
const record = await db.find("otps", { userId })
const valid = isOTPValid(record.otp, record.expiresAt, userInputOtp)
if (valid) {
// proceed
} else {
// retry or deny
}
Best Practices
- Set a short expiry (5–10 minutes)
- Delete or mark used/expired OTPs to prevent reuse
- Ensure
storedOtp
andprovidedOtp
lengths match - Rely on built-in
crypto.timingSafeEqual
for security
callHook(fn, context)
Safely invokes a user-provided hook (sync or async), catching and logging errors without interrupting the flow.
Signature
async function callHook(
fn: Function | null | undefined,
context: Record<string, any>
): Promise<void>
Behavior
- No-ops if
fn
is not a function - Awaits the result if
fn
returns a promise - Catches and logs any errors internally
Examples
- Single hook
import { callHook } from "modules/utils/hookUtils"
async function onDataSave({ data, logger }) {
if (!data.id) throw new Error("Missing ID")
logger.info("Data saved:", data)
}
await callHook(onDataSave, { data: { id: 123 }, logger: console })
// Errors are logged as “[Hook Error]” but do not bubble up
- Multiple hooks
import { callHook } from "modules/utils/hookUtils"
const hooks = [
async ctx => { /* hook A */ },
ctx => { /* hook B */ },
null, // skipped
() => { throw new Error() } // caught and logged
]
for (const hook of hooks) {
await callHook(hook, { user: { id: 1 }, timestamp: Date.now() })
}
Tips
- Keep
context
minimal; include only needed properties - Use distinct logging prefixes for easier tracing
- For parallel execution, wrap in
Promise.all
but handle logs individually:await Promise.all(hooks.map(h => callHook(h, ctx)))
Extending & Contribution
This section explains how to adapt Light-Auth to your project’s needs and contribute improvements back to the repository.
Customizing Library Behavior
Configuring the Error-Handling Hook (hooks.onError)
Allow your app to intercept internal setupAuth failures without leaking stack traces or crashing.
• setupAuth wraps critical checks in callHook(merged.hooks.onError, { type, error })
.
• Your onError
hook runs with { type: string, error: Error }
before the error is thrown.
Example:
import express from "express";
import { setupAuth } from "@daksh-dev/light-auth";
const app = express();
await setupAuth(app, {
jwtSecret: process.env.JWT_SECRET,
db: mongoose.connection,
hooks: {
onError: async ({ type, error }) => {
// log to Sentry, suppress in dev
console.log(`Auth setup failed at phase="${type}"`, error.message);
await monitoringService.notify({ module: "setupAuth", phase: type, message: error.stack });
}
}
});
Tips:
- Mark
onError
async if you call external services. - Don’t rethrow inside the hook; errors are caught by
callHook
. - Use
type
to distinguish phases (e.g.,"setup"
,"db"
).
Overriding the User Model (createUserModel
)
Provide a custom Mongoose User model or scaffold a default one.
Basic usage:
import mongoose from "mongoose";
import { createUserModel } from "@daksh-dev/light-auth";
const roles = ["user", "admin"];
const authCfg = {
// To override default:
// User: require("./models/MyUser.js").default
};
async function init() {
// Ensure mongoose.connect() has run
const User = await createUserModel(authCfg, roles, mongoose);
const u = await User.create({ email: "jane@doe.com", password: "s3cr3t" });
console.log(u);
}
init();
Behavior:
- Returns
config.User
if provided. - Returns
db.models.User
if registered. - Attempts to load
models/User.js
. - In non-production, generates and writes a scaffold if missing; errors out in production.
Tips:
- Commit the generated
models/User.js
to avoid runtime scaffolding. - Supply
config.User
to use custom schemas or plugins.
Replacing the Mail Sender (defaultSendMail
)
defaultSendMail
logs OTPs and links to the console for development.
Signature:
async function defaultSendMail({
email: string,
otp: string,
type: string,
url?: string
}): Promise<void>
Usage:
import { defaultSendMail } from "@daksh-dev/light-auth";
async function sendLoginOtp(userEmail) {
const otp = generateOtp();
await defaultSendMail({ email: userEmail, otp, type: "login" });
}
await defaultSendMail({
email: "user@domain.com",
otp: "654321",
type: "reset",
url: `https://app.example.com/reset?token=${token}`
});
Tips:
- Use only in local or test environments.
- Replace with your SMTP provider in production.
- Ensure your OTP generator produces time-bound, single-use codes.
Contribution Guidelines
Follow these steps to submit fixes or enhancements.
1. Setup Development Environment
git clone https://github.com/Daksh-Dixit21/light-auth.git
cd light-auth
pnpm install
pnpm test # ensure existing tests pass
2. Code Style & Testing
- Use Node.js 20+ and ES modules.
- Run
pnpm lint
andpnpm test
before committing. - Add tests under
tests/
for new features or bug fixes.
3. Branching & Commit Messages
- Create feature branches:
git checkout -b feature/your-feature-name
. - Write descriptive commit messages:
<area>: <short description>
(e.g.,hooks: add onError metadata
).
4. Pull Request Process
- Push your branch to GitHub:
git push origin feature/your-feature-name
. - Open a PR against
main
with the following:- Purpose and high-level summary.
- Link to related issue or ticket.
- Screenshots or logs if applicable.
- Address review comments and squash/fixup commits as needed.
5. CI & Publishing
- The GitHub Actions workflow (
.github/workflows/ci.yml
) runs lint, tests, and publishes on release. - To publish a new version:
git tag vX.Y.Z git push origin vX.Y.Z gh release create vX.Y.Z --title "vX.Y.Z" --notes "Changelog..."
- Ensure
NPM_TOKEN
is set in repo Secrets for automated publishing.
Thank you for helping improve Light-Auth! We welcome all contributions.