The Problem
Every Node.js developer knows the drill. You're building an Express app, and suddenly you need:
- User authentication
- Role-based access control
- Admin panels
- Audit logs
- Rate limiting
- 2FA support
That's days of coding, testing, and debugging security vulnerabilities.
The Solution
Meet authwall — a complete, production-ready authentication system that drops into any Express + MongoDB app in 60 seconds.
// That's literally it
const { authwall } = require('authwall');
authwall(app, { config: { jwt: { secret: process.env.JWT_SECRET } } });
What You Get Out of the Box
| Feature | Status |
|---|---|
| Email OTP login | ✅ |
| JWT authentication | ✅ |
| Role-based access (RBAC) | ✅ |
| Full admin REST API | ✅ |
| User suspension/banning | ✅ |
| Two-factor authentication (TOTP) | ✅ |
| Audit logs with request IDs | ✅ |
| Rate limiting | ✅ |
| CSV user export | ✅ |
| Bulk operations | ✅ |
| Impersonation mode | ✅ |
| Event system for webhooks | ✅ |
Why Developers Love It
"I was about to spend 3 days building auth. authwall took 10 minutes." — Senior Backend Engineer
"The audit logs alone saved us during a security compliance audit." — CTO, SaaS Startup
"Finally, an auth package that doesn't force its own user model." — Full-stack Developer
Quick Start
npm install authwall
const express = require('express');
const mongoose = require('mongoose');
const { authwall } = require('authwall');
const app = express();
app.use(express.json());
mongoose.connect('mongodb://localhost:27017/myapp');
authwall(app, {
seed: true,
config: {
jwt: { secret: process.env.JWT_SECRET },
email: {
from: 'noreply@myapp.com',
transport: { host: 'smtp.gmail.com', port: 587, auth: {...} }
}
}
});
app.listen(3000);
That's it. You now have:
-
POST /api/auth/otp/send— send login code -
POST /api/auth/otp/verify— verify & get JWT -
GET /api/auth/me— get current user -
GET /api/admin/users— list all users -
POST /api/admin/users/:id/suspend— suspend a user - ...and 25+ more endpoints
Real-World Example
Want to let admins suspend bad actors?
# Admin logs in
curl -X POST /api/auth/otp/send -d '{"email":"admin@company.com"}'
curl -X POST /api/auth/otp/verify -d '{"email":"admin@company.com","code":"123456"}'
# → Returns JWT token
# Suspend a problematic user
curl -X POST /api/admin/users/user_123/suspend \
-H "Authorization: Bearer <token>"
Want to know when users log in?
const { events, EVENTS } = require('authwall');
events.on(EVENTS.LOGIN_SUCCESS, (data) => {
console.log(`✅ ${data.email} logged in from ${data.ip}`);
// Send to Slack, DataDog, webhook, etc.
});
Works With Your Existing User Model
// Your existing User schema
const userSchema = new mongoose.Schema({
name: String,
email: String,
subscriptionTier: String
});
// Just add authwall fields
const { addUserAdminFields } = require('authwall');
addUserAdminFields(userSchema); // Adds roles, status, 2FA fields
authwall(app, { getUserModel: () => mongoose.model('User', userSchema) });
Security Built-In
- No default JWT secrets — you must provide one
- Rate limiting on OTP endpoints — prevents brute force
- Admin self-protection — can't suspend/delete themselves
- Audit logs with request IDs — trace every admin action
- Session termination — revoke all user sessions with one call
What's Coming
- [ ] Social logins (Google, GitHub)
- [ ] Password-based authentication
- [ ] WebAuthn/Passkeys support
- [ ] OpenAPI/Swagger generation
Get Started Today
npm install authwall
📖 Full Documentation
🐛 Report Issues
⭐ Star on GitHub
About the Author
Susheel Kumar — Full-stack developer building tools that make developers' lives easier.
📧 susheelhbti@gmail.com — Open for freelance & full-time opportunities.
📘 Complete User Guide
Getting Started with authwall
Table of Contents
- Installation
- Basic Setup
- Configuration
- Authentication Flow
- Admin Operations
- Role-Based Access Control
- Two-Factor Authentication
- Event System
- Production Deployment
- Troubleshooting
1. Installation
npm install authwall
Requirements:
- Node.js ≥ 18
- Express ≥ 4
- Mongoose ≥ 8
- MongoDB running instance
2. Basic Setup
Minimal Working Example
Create app.js:
const express = require('express');
const mongoose = require('mongoose');
const { authwall } = require('authwall');
const app = express();
app.use(express.json());
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp');
// Initialize authwall
authwall(app, {
seed: true, // Creates default roles: admin, user, moderator
config: {
jwt: {
secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this',
},
email: {
from: 'noreply@yourapp.com',
transport: {
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
},
},
},
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Environment Variables (.env)
# Required
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
# Email configuration
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password
# Optional
CORS_ORIGIN=https://yourapp.com
PORT=3000
MONGODB_URI=mongodb://localhost:27017/myapp
3. Configuration
Full Configuration Options
authwall(app, {
// Seed default roles on startup
seed: true,
// Auto-clean expired OTPs every 15min
cleanupCron: true,
// Use your own User model (optional)
getUserModel: () => require('./models/User'),
config: {
// Route prefix for all endpoints
routePrefix: '/api',
// Global middleware before auth routes
routeMiddleware: [logger, cors],
// OTP settings
otp: {
expiresInMinutes: 5,
maxAttempts: 3,
rateLimitCount: 3,
rateLimitMinutes: 5,
codeLength: 6,
},
// JWT settings
jwt: {
secret: process.env.JWT_SECRET, // Required
expiresIn: '7d', // 7 days
},
// Role defaults
defaultRoleSlug: 'user',
adminRoleSlug: 'admin',
// Auto-register new users on OTP verify
autoRegister: true,
// Pagination default
perPage: 15,
// Rate limiting
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // max OTP attempts
},
// CORS
cors: {
enabled: true,
origin: process.env.CORS_ORIGIN || '*',
credentials: true,
},
// Email
email: {
from: 'noreply@yourapp.com',
transport: { /* nodemailer config */ },
},
},
});
4. Authentication Flow
Step 1: Send OTP
POST /api/auth/otp/send
Content-Type: application/json
{
"email": "user@example.com"
}
Response:
{
"success": true,
"message": "OTP sent successfully",
"expires_in_minutes": 5
}
Step 2: Verify OTP
POST /api/auth/otp/verify
Content-Type: application/json
{
"email": "user@example.com",
"code": "123456"
}
Response:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"_id": "65abc123...",
"email": "user@example.com",
"role": "user",
"status": "active"
}
}
Step 3: Use JWT Token
GET /api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Step 4: Logout
POST /api/auth/logout
Authorization: Bearer <token>
5. Admin Operations
First Admin Setup
The first user who logs in with autoRegister: true gets the user role. To create an admin:
Option 1: Via MongoDB directly
// Run in MongoDB shell or Compass
db.users.updateOne(
{ email: "admin@example.com" },
{ $set: { role: "admin", status: "active" } }
)
Option 2: Create admin programmatically
const User = mongoose.model('User');
await User.create({
email: 'admin@example.com',
role: 'admin',
status: 'active'
});
Admin API Endpoints
List Users with Filters
GET /api/admin/users
Authorization: Bearer <admin-token>
# With filters
GET /api/admin/users?status=suspended&role=user&search=john&page=2&sort_by=createdAt_desc
Get User Details
GET /api/admin/users/:userId
Suspend User
POST /api/admin/users/:userId/suspend
Authorization: Bearer <admin-token>
# Optional body
{
"reason": "Violated terms of service"
}
Temporary Ban
POST /api/admin/users/:userId/temporary-ban
{
"duration_hours": 24,
"reason": "Spamming"
}
Force Password Reset
POST /api/admin/users/:userId/force-password-reset
# Sends email to user with reset link
Impersonate User
POST /api/admin/users/:userId/impersonate
# Returns a special JWT that acts as the target user
# Stop impersonation
POST /api/admin/users/stop-impersonation
Terminate All Sessions
POST /api/admin/users/:userId/terminate-sessions
# Revokes all refresh tokens for this user
Bulk Operations
# Bulk suspend
POST /api/admin/users/bulk/suspend
{
"user_ids": ["id1", "id2", "id3"]
}
# Bulk assign role
POST /api/admin/users/bulk/assign-role
{
"user_ids": ["id1", "id2"],
"role": "moderator"
}
Export Users to CSV
GET /api/admin/users/export
Authorization: Bearer <admin-token>
# Downloads CSV with all user data
View Audit Logs
GET /api/admin/admin-logs
# Shows all admin actions with timestamps, IPs, request IDs
Statistics
GET /api/admin/statistics
Response:
{
"total_users": 1542,
"active_users": 1289,
"suspended_users": 42,
"admins": 3,
"users_by_role": {
"admin": 3,
"moderator": 12,
"user": 1527
},
"users_by_status": {
"active": 1289,
"suspended": 42,
"banned": 211
}
}
6. Role-Based Access Control
Default Roles
| Role | Permissions |
|---|---|
admin |
Full access to all admin endpoints |
moderator |
Can view users, suspend/unsuspend, but cannot delete or access sensitive settings |
user |
Can only access their own profile |
Creating Custom Roles
const Role = mongoose.model('Role');
await Role.create({
name: 'Content Manager',
slug: 'content-manager',
permissions: [
'users:read',
'posts:create',
'posts:edit',
'posts:delete'
]
});
Protecting Routes with Middleware
const { makeAuthMiddleware, makeRoleMiddleware } = require('authwall');
const auth = makeAuthMiddleware(config, () => User);
const requireModerator = makeRoleMiddleware('moderator');
const requireAdmin = makeRoleMiddleware('admin');
// Public route
app.get('/api/public', (req, res) => {...});
// Any authenticated user
app.get('/api/profile', auth, (req, res) => {
res.json(req.user);
});
// Moderator only
app.post('/api/moderate/comment', auth, requireModerator, handler);
// Admin only
app.delete('/api/admin/users/:id', auth, requireAdmin, handler);
7. Two-Factor Authentication
Enabling 2FA for a User
const { generateTOTPSecret } = require('authwall');
// Generate secret and QR code
const { secret, qrCodeUrl } = await generateTOTPSecret(user.email);
// Store secret temporarily or with user
user.twoFactorSecret = secret;
await user.save();
// Send QR code to user (via email or display in UI)
Verifying 2FA During Login
Step 1: Normal OTP verification
POST /api/auth/otp/verify
{
"email": "user@example.com",
"code": "123456"
}
If 2FA is enabled, response:
{
"requires_2fa": true,
"otp_id": "otp_session_123",
"message": "Two-factor authentication required."
}
Step 2: Submit TOTP code
POST /api/auth/otp/verify
{
"otp_id": "otp_session_123",
"code": "123456",
"totp_token": "654321"
}
Or use backup code:
{
"otp_id": "otp_session_123",
"code": "123456",
"backup_code": "ABCD-EFGH-IJKL-MNOP"
}
Disabling 2FA (Admin)
POST /api/admin/users/:userId/remove-2fa
Authorization: Bearer <admin-token>
8. Event System
Available Events
const { events, EVENTS } = require('authwall');
// Authentication events
events.on(EVENTS.LOGIN_SUCCESS, (data) => {
// data: { userId, email, ip, userAgent, timestamp }
});
events.on(EVENTS.LOGIN_FAILED, (data) => {
// data: { email, ip, reason }
});
events.on(EVENTS.OTP_SENT, (data) => {
// data: { email, expiresIn }
});
// User management events
events.on(EVENTS.USER_CREATED, (data) => {
// data: { user, createdBy }
});
events.on(EVENTS.USER_SUSPENDED, (data) => {
// data: { userId, email, reason, suspendedBy }
});
// Admin events
events.on(EVENTS.ADMIN_ACTION, (data) => {
// data: { adminId, action, target, ip, requestId }
});
// 2FA events
events.on(EVENTS.TFA_ENABLED, (data) => {
// data: { userId, email }
});
events.on(EVENTS.TFA_FAILED, (data) => {
// data: { userId, email, ip, attemptCount }
});
Real-World Examples
Send Slack Alert on Suspicious Activity:
const { events, EVENTS } = require('authwall');
const axios = require('axios');
events.on(EVENTS.LOGIN_FAILED, async (data) => {
if (data.reason === 'too_many_attempts') {
await axios.post(process.env.SLACK_WEBHOOK, {
text: `🚨 Brute force attempt detected on ${data.email} from ${data.ip}`
});
}
});
Track User Metrics:
const metrics = {
logins: 0,
newUsers: 0,
suspensions: 0
};
events.on(EVENTS.LOGIN_SUCCESS, () => metrics.logins++);
events.on(EVENTS.USER_CREATED, () => metrics.newUsers++);
events.on(EVENTS.USER_SUSPENDED, () => metrics.suspensions++);
// Expose metrics endpoint
app.get('/metrics', (req, res) => res.json(metrics));
Send Welcome Email:
events.on(EVENTS.USER_CREATED, async (data) => {
await sendWelcomeEmail(data.user.email);
});
9. Production Deployment
Security Checklist
- [ ] Set strong
JWT_SECRET(32+ chars, random) - [ ] Enable HTTPS
- [ ] Configure CORS properly (not
*in production) - [ ] Set appropriate rate limits
- [ ] Use environment variables for all secrets
- [ ] Enable MongoDB authentication
- [ ] Regularly backup audit logs
- [ ] Monitor admin logs for suspicious activity
Multi-Server Setup (Load Balancing)
For multiple server instances, configure shared rate limiting with Redis:
npm install rate-limit-redis ioredis
const { rateLimit } = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const sharedLimiter = rateLimit({
store: new RedisStore({
client: new Redis(process.env.REDIS_URL)
}),
windowMs: 15 * 60 * 1000,
max: 5
});
// Apply to authwall routes
authwall(app, {
config: {
rateLimit: sharedLimiter
}
});
Performance Optimization
// Use indexes for large user bases
const User = mongoose.model('User');
User.createIndexes({ email: 1, status: 1, role: 1 });
// Enable Mongoose debug in development (not production)
mongoose.set('debug', process.env.NODE_ENV === 'development');
Monitoring with PM2
# ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
instances: 2,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
JWT_SECRET: process.env.JWT_SECRET
}
}]
};
Docker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGODB_URI=mongodb://mongodb:27017/myapp
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongodb
mongodb:
image: mongo:6
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
10. Troubleshooting
Common Issues
Issue: "JWT secret is required"
# Set environment variable
export JWT_SECRET="your-strong-secret-here"
# Or in .env file
echo "JWT_SECRET=your-secret" >> .env
Issue: OTP emails not sending
// Test email configuration
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTestAccount((err, account) => {
console.log('Test account:', account);
});
Issue: User can't login after suspension
// Check user status
const user = await User.findOne({ email: 'user@example.com' });
console.log('Status:', user.status); // Should be 'active'
console.log('Suspended until:', user.suspendedUntil);
// Unsuspend manually
user.status = 'active';
user.suspendedUntil = null;
await user.save();
Issue: Rate limiting too aggressive
// Increase limits
authwall(app, {
config: {
rateLimit: {
windowMs: 15 * 60 * 1000,
max: 20 // Allow 20 attempts per 15 minutes
}
}
});
Issue: "User model not found"
// Register model before authwall
const User = mongoose.model('User', userSchema);
authwall(app, { getUserModel: () => User });
Debug Mode:
// Enable debug logging
process.env.DEBUG = 'authwall:*';
const { authwall } = require('authwall');
Support & Community
- GitHub Issues: Report bugs
- NPM Package: authwall
- Author Email: susheelhbti@gmail.com
License
MIT © Susheel Kumar
Top comments (0)