DEV Community

susheel kumar
susheel kumar

Posted on

Stop Building Auth From Scratch — Meet authwall: The Plug-and-Play Auth System for Express

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 } } });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

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.
});
Enter fullscreen mode Exit fullscreen mode

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) });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

📖 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

  1. Installation
  2. Basic Setup
  3. Configuration
  4. Authentication Flow
  5. Admin Operations
  6. Role-Based Access Control
  7. Two-Factor Authentication
  8. Event System
  9. Production Deployment
  10. Troubleshooting

1. Installation

npm install authwall
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */ },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

4. Authentication Flow

Step 1: Send OTP

POST /api/auth/otp/send
Content-Type: application/json

{
  "email": "user@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "success": true,
  "message": "OTP sent successfully",
  "expires_in_minutes": 5
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Verify OTP

POST /api/auth/otp/verify
Content-Type: application/json

{
  "email": "user@example.com",
  "code": "123456"
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "_id": "65abc123...",
    "email": "user@example.com",
    "role": "user",
    "status": "active"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use JWT Token

GET /api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Enter fullscreen mode Exit fullscreen mode

Step 4: Logout

POST /api/auth/logout
Authorization: Bearer <token>
Enter fullscreen mode Exit fullscreen mode

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" } }
)
Enter fullscreen mode Exit fullscreen mode

Option 2: Create admin programmatically

const User = mongoose.model('User');
await User.create({
  email: 'admin@example.com',
  role: 'admin',
  status: 'active'
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Get User Details

GET /api/admin/users/:userId
Enter fullscreen mode Exit fullscreen mode

Suspend User

POST /api/admin/users/:userId/suspend
Authorization: Bearer <admin-token>

# Optional body
{
  "reason": "Violated terms of service"
}
Enter fullscreen mode Exit fullscreen mode

Temporary Ban

POST /api/admin/users/:userId/temporary-ban
{
  "duration_hours": 24,
  "reason": "Spamming"
}
Enter fullscreen mode Exit fullscreen mode

Force Password Reset

POST /api/admin/users/:userId/force-password-reset
# Sends email to user with reset link
Enter fullscreen mode Exit fullscreen mode

Impersonate User

POST /api/admin/users/:userId/impersonate
# Returns a special JWT that acts as the target user
Enter fullscreen mode Exit fullscreen mode
# Stop impersonation
POST /api/admin/users/stop-impersonation
Enter fullscreen mode Exit fullscreen mode

Terminate All Sessions

POST /api/admin/users/:userId/terminate-sessions
# Revokes all refresh tokens for this user
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Export Users to CSV

GET /api/admin/users/export
Authorization: Bearer <admin-token>
# Downloads CSV with all user data
Enter fullscreen mode Exit fullscreen mode

View Audit Logs

GET /api/admin/admin-logs
# Shows all admin actions with timestamps, IPs, request IDs
Enter fullscreen mode Exit fullscreen mode

Statistics

GET /api/admin/statistics
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
  ]
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Verifying 2FA During Login

Step 1: Normal OTP verification

POST /api/auth/otp/verify
{
  "email": "user@example.com",
  "code": "123456"
}
Enter fullscreen mode Exit fullscreen mode

If 2FA is enabled, response:

{
  "requires_2fa": true,
  "otp_id": "otp_session_123",
  "message": "Two-factor authentication required."
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Submit TOTP code

POST /api/auth/otp/verify
{
  "otp_id": "otp_session_123",
  "code": "123456",
  "totp_token": "654321"
}
Enter fullscreen mode Exit fullscreen mode

Or use backup code:

{
  "otp_id": "otp_session_123",
  "code": "123456",
  "backup_code": "ABCD-EFGH-IJKL-MNOP"
}
Enter fullscreen mode Exit fullscreen mode

Disabling 2FA (Admin)

POST /api/admin/users/:userId/remove-2fa
Authorization: Bearer <admin-token>
Enter fullscreen mode Exit fullscreen mode

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 }
});
Enter fullscreen mode Exit fullscreen mode

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}`
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Send Welcome Email:

events.on(EVENTS.USER_CREATED, async (data) => {
  await sendWelcomeEmail(data.user.email);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
  }
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
    }
  }]
};
Enter fullscreen mode Exit fullscreen mode

Docker Deployment

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode
# 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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Issue: OTP emails not sending

// Test email configuration
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTestAccount((err, account) => {
  console.log('Test account:', account);
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Issue: Rate limiting too aggressive

// Increase limits
authwall(app, {
  config: {
    rateLimit: {
      windowMs: 15 * 60 * 1000,
      max: 20  // Allow 20 attempts per 15 minutes
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Issue: "User model not found"

// Register model before authwall
const User = mongoose.model('User', userSchema);
authwall(app, { getUserModel: () => User });
Enter fullscreen mode Exit fullscreen mode

Debug Mode:

// Enable debug logging
process.env.DEBUG = 'authwall:*';
const { authwall } = require('authwall');
Enter fullscreen mode Exit fullscreen mode

Support & Community


License

MIT © Susheel Kumar

Top comments (0)