@uoj-lk/auth-node
v1.2.0
Published
Lightweight Node.js authentication middleware for MyUOJ Auth Service with JWT validation, RBAC, and Redis caching for microservices
Maintainers
Readme
@uoj-lk/auth-node
Production-ready authentication middleware for Node.js microservices with JWT validation, RBAC, Redis caching, and enterprise-grade security.
Why @uoj-lk/auth-node?
Built for the University of Jaffna's authentication infrastructure, this middleware provides enterprise-grade security with minimal overhead. Perfect for microservices that need centralized authentication without reinventing the wheel.
Performance: 95% faster auth with Redis caching (5-10ms vs 100-200ms)
Security: SSRF protection, cache integrity validation, token blacklisting
Observability: Built-in metrics, structured logging, health checks
DX: Drop-in Express middleware with TypeScript support
Quick Start
Installation
npm install @uoj-lk/auth-node express cookie-parser
# Optional: For caching
npm install redisBasic Usage
const express = require("express");
const cookieParser = require("cookie-parser");
const authNode = require("@uoj-lk/auth-node");
const app = express();
app.use(express.json());
app.use(cookieParser());
// Initialize with secure defaults
const auth = authNode.init({
authServiceUrl: process.env.AUTH_SERVICE_URL,
jwtSecret: process.env.JWT_SECRET, // Min 32 chars
jwtIssuer: "myuoj-auth",
jwtAudience: "myuoj-services",
});
// Protected route
app.get("/api/protected", auth.authenticateJWT, (req, res) => {
res.json({
user: req.user,
roles: req.roles,
permissions: req.permissions,
});
});
app.listen(3000);Features
🔐 JWT Authentication
Validates JWT tokens from cookies or Authorization headers with automatic caching.
// Require authentication
app.get("/api/users", auth.authenticateJWT, (req, res) => {
// req.user, req.roles, req.permissions available
res.json({ users: [] });
});
// Optional authentication
app.get("/api/public", auth.optionalAuth, (req, res) => {
const message = req.user ? `Welcome ${req.user.username}` : "Welcome guest";
res.json({ message });
});🛡️ Role-Based Access Control (RBAC)
// Require specific role
app.delete(
"/api/admin/users/:id",
auth.authenticateJWT,
auth.requireRole("admin"),
(req, res) => {
// Only admins can access
}
);
// Require specific permission
app.post(
"/api/users",
auth.authenticateJWT,
auth.requirePermission("users:write"),
(req, res) => {
// Only users with 'users:write' permission
}
);
// Require multiple permissions (ALL)
app.put(
"/api/users/:id",
auth.authenticateJWT,
auth.requirePermission("users:read", "users:write"),
(req, res) => {
// User must have BOTH permissions
}
);
// Require any permission (OR)
app.get(
"/api/dashboard",
auth.authenticateJWT,
auth.requireAnyPermission("users:read", "posts:read", "dashboard:view"),
(req, res) => {
// User needs at least ONE permission
}
);⚡ Redis Caching
Reduce auth service load by 90% with intelligent caching:
const { createClient } = require("redis");
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const auth = authNode.init({
authServiceUrl: process.env.AUTH_SERVICE_URL,
jwtSecret: process.env.JWT_SECRET,
redis,
useCache: true,
cacheTTL: 300, // 5 minutes
});
// First request: ~100-200ms (validates with auth service)
// Cached requests: ~5-10ms (reads from Redis)
// Automatic cache invalidation on user/role changesCache Features:
- SHA256 integrity validation prevents cache poisoning
- Automatic fallback if Redis unavailable
- TTL-based expiration (default 5 minutes)
- Manual invalidation support
🔒 Token Blacklisting
Revoke tokens immediately (logout, password reset, compromise):
app.post("/api/logout", auth.authenticateJWT, async (req, res) => {
const jwt = require("jsonwebtoken");
const payload = jwt.decode(req.token);
if (payload?.jti) {
// Blacklist until natural expiration
const ttl = payload.exp - Math.floor(Date.now() / 1000);
await auth.blacklistToken(payload.jti, ttl);
}
res.json({ success: true });
});📊 Observability
Metrics
// JSON metrics
app.get("/api/metrics", (req, res) => {
res.json(auth.getMetrics());
});
// Prometheus format
app.get("/metrics", (req, res) => {
res.set("Content-Type", "text/plain");
res.send(auth.getPrometheusMetrics());
});Available Metrics:
- Auth attempts, successes, failures
- Cache hit/miss rates
- RBAC checks and denials
- Average response times
- Error counts by type
Health Checks
app.get("/health", async (req, res) => {
const health = await auth.getHealthStatus();
res.status(health.healthy ? 200 : 503).json(health);
});Response:
{
"healthy": true,
"timestamp": "2025-12-14T10:30:00Z",
"components": {
"authService": { "status": "healthy" },
"cache": { "status": "healthy", "available": true }
},
"metrics": {
"authAttempts": 1250,
"cacheHitRate": 0.884
}
}Structured Logging
const { logger } = require("@uoj-lk/auth-node");
logger.info("User authenticated", { userId: req.user.id });
logger.warn("High failure rate detected", { failures: 50 });
logger.error("Auth service unreachable", { error: err.message });Security
✅ Enterprise-Grade Protection
| Feature | Implementation | | ----------------------- | ------------------------------------------------------- | | JWT Secret Strength | Enforces 32+ characters with entropy validation | | SSRF Protection | Blocks private IPs, localhost, cloud metadata endpoints | | Cache Integrity | SHA256 hashes prevent poisoning attacks | | Token Revocation | Redis-backed blacklist for immediate invalidation | | Input Sanitization | Validates roles/permissions against injection | | Error Handling | Generic messages in production, detailed server logs |
🔒 Production Checklist
# Generate strong secret (32+ characters)
openssl rand -base64 32
# Environment variables
AUTH_SERVICE_URL=https://auth.myuoj.ac.lk # HTTPS required
JWT_SECRET=your-cryptographically-random-secret
REDIS_URL=redis://redis-server:6379
NODE_ENV=productionRequired:
- [ ] JWT secret ≥32 characters
- [ ] Auth service URL uses HTTPS
- [ ] Redis configured for caching
- [ ]
NODE_ENV=productionset - [ ] CORS configured for trusted origins
- [ ] Rate limiting enabled
- [ ] Health checks monitored
📖 Full Security Documentation | Security Audit Report
API Reference
Configuration
const auth = authNode.init({
// Required
authServiceUrl: string, // Auth service URL (HTTPS in production)
jwtSecret: string, // JWT secret (min 32 characters)
// Optional JWT validation
jwtIssuer: string, // Expected JWT issuer
jwtAudience: string, // Expected JWT audience
// Optional caching
redis: RedisClient, // Redis client instance
useCache: boolean, // Enable caching (default: true)
cacheTTL: number, // Cache TTL in seconds (default: 300)
// Optional configuration
cookieName: string, // Cookie name (default: 'accessToken')
logLevel: string, // error|warn|info|debug (default: 'info')
debug: boolean, // Detailed errors (default: false)
allowInternalUrls: boolean, // Dev-only: allow localhost (default: false)
});Middleware
// Authentication
auth.authenticateJWT; // Require JWT authentication
auth.optionalAuth; // Optional authentication
// Authorization (RBAC)
auth.requireRole(...roles); // Require specific role(s)
auth.requirePermission(...permissions); // Require all permissions
auth.requireAnyPermission(...permissions); // Require any permission
// Error handling
auth.errorHandler; // Error middleware (use last)
auth.notFoundHandler(); // 404 handlerServices
// Cache management
auth.initCache(redisClient);
auth.getCachedUser(userId);
auth.cacheUser(userId, userData, ttl);
auth.invalidateUserCache(userId);
auth.clearAllCache();
auth.getCacheStats();
// Token blacklisting
auth.blacklistToken(jti, ttl);
auth.isTokenBlacklisted(jti);
// Health & metrics
auth.getHealthStatus();
auth.checkAuthServiceHealth();
auth.getMetrics();
auth.getPrometheusMetrics();
auth.resetMetrics();
// Utilities
auth.hasPermission(req, permission);
auth.hasRole(req, role);
auth.isAuthenticated(req);
auth.getUserId(req);
auth.getUsername(req);
auth.extractToken(req, cookieName);Request Object Enhancement
After authenticateJWT, the request object is enriched:
req.user; // { id, username, email, status, ... }
req.roles; // ['admin', 'editor']
req.permissions; // ['users:read', 'users:write']
req.roleDetails; // [{ name: 'admin', permissions: [...] }]
req.organizations; // [{ id, name, ... }]
req.token; // Original JWT stringAdvanced Examples
Complete Express Server
const express = require("express");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const { createClient } = require("redis");
const authNode = require("@uoj-lk/auth-node");
const app = express();
// Middleware
app.use(express.json());
app.use(cookieParser());
app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true }));
// Initialize Redis
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// Initialize auth
const auth = authNode.init({
authServiceUrl: process.env.AUTH_SERVICE_URL,
jwtSecret: process.env.JWT_SECRET,
jwtIssuer: "myuoj-auth",
jwtAudience: "myuoj-services",
redis,
cacheTTL: 300,
logLevel: "info",
});
// Public routes
app.get("/api/public/status", (req, res) => {
res.json({ status: "ok" });
});
// Protected routes
app.get(
"/api/users",
auth.authenticateJWT,
auth.requirePermission("users:read"),
async (req, res) => {
const users = await getUsers();
res.json({ users });
}
);
app.post(
"/api/users",
auth.authenticateJWT,
auth.requirePermission("users:write"),
async (req, res) => {
const user = await createUser(req.body);
res.json({ user });
}
);
app.delete(
"/api/users/:id",
auth.authenticateJWT,
auth.requirePermission("users:delete"),
async (req, res) => {
await deleteUser(req.params.id);
res.json({ success: true });
}
);
// Admin-only routes
app.get(
"/api/admin/stats",
auth.authenticateJWT,
auth.requireRole("admin"),
async (req, res) => {
const stats = await getSystemStats();
res.json({ stats });
}
);
// Logout endpoint
app.post("/api/logout", auth.authenticateJWT, async (req, res) => {
const jwt = require("jsonwebtoken");
const payload = jwt.decode(req.token);
if (payload?.jti) {
const ttl = payload.exp - Math.floor(Date.now() / 1000);
await auth.blacklistToken(payload.jti, ttl);
}
res.json({ success: true });
});
// Health & metrics
app.get("/health", async (req, res) => {
const health = await auth.getHealthStatus();
res.status(health.healthy ? 200 : 503).json(health);
});
app.get("/metrics", (req, res) => {
res.set("Content-Type", "text/plain");
res.send(auth.getPrometheusMetrics());
});
// Error handling
app.use(auth.errorHandler);
app.use(auth.notFoundHandler());
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Programmatic Permission Checks
app.get("/api/users/:id", auth.authenticateJWT, async (req, res) => {
const userId = req.params.id;
const user = await getUser(userId);
// Only show sensitive data if user has permission
if (auth.hasPermission(req, "users:read:sensitive")) {
user.email = await getEmail(userId);
user.phone = await getPhone(userId);
}
// Only admins can see deleted users
if (user.status === "deleted" && !auth.hasRole(req, "admin")) {
return res.status(404).json({ error: "User not found" });
}
res.json({ user });
});Cache Invalidation Strategy
// Invalidate cache when user data changes
app.put(
"/api/users/:id",
auth.authenticateJWT,
auth.requirePermission("users:write"),
async (req, res) => {
const userId = req.params.id;
await updateUser(userId, req.body);
// Clear cache for updated user
await auth.invalidateUserCache(userId);
res.json({ success: true });
}
);
// Invalidate cache when roles change
app.post(
"/api/users/:id/roles",
auth.authenticateJWT,
auth.requirePermission("users:roles:write"),
async (req, res) => {
const userId = req.params.id;
await updateUserRoles(userId, req.body.roles);
// Clear cache since permissions changed
await auth.invalidateUserCache(userId);
res.json({ success: true });
}
);Multiple Permission Strategies
// Flexible permission requirements
const requireUserAccess = (req, res, next) => {
const targetUserId = req.params.id;
const currentUserId = auth.getUserId(req);
// Allow if user is accessing their own data OR has admin role
if (targetUserId === currentUserId || auth.hasRole(req, "admin")) {
return next();
}
// Or if user has the specific permission
if (auth.hasPermission(req, "users:read:all")) {
return next();
}
res.status(403).json({ error: "Insufficient permissions" });
};
app.get(
"/api/users/:id",
auth.authenticateJWT,
requireUserAccess,
async (req, res) => {
const user = await getUser(req.params.id);
res.json({ user });
}
);TypeScript Support
Full type definitions included:
import {
init,
AuthNodeConfig,
AuthRequest,
AuthMiddleware,
UserData,
HealthStatus,
Metrics,
} from "@uoj-lk/auth-node";
import { Request, Response, NextFunction } from "express";
const config: AuthNodeConfig = {
authServiceUrl: process.env.AUTH_SERVICE_URL!,
jwtSecret: process.env.JWT_SECRET!,
redis: redisClient,
cacheTTL: 300,
};
const auth = init(config);
// Typed route handler
app.get(
"/api/users",
auth.authenticateJWT,
(req: AuthRequest, res: Response) => {
const userId: number = req.user!.id;
const username: string = req.user!.username;
const permissions: string[] = req.permissions;
res.json({ userId, username, permissions });
}
);
// Typed health check
app.get("/health", async (req: Request, res: Response) => {
const health: HealthStatus = await auth.getHealthStatus();
res.status(health.healthy ? 200 : 503).json(health);
});
// Typed metrics
app.get("/metrics", (req: Request, res: Response) => {
const metrics: Metrics = auth.getMetrics();
res.json(metrics);
});Environment Variables
# Required
AUTH_SERVICE_URL=https://auth.myuoj.ac.lk
JWT_SECRET=your-cryptographically-random-32plus-character-secret
# Optional - Redis
REDIS_URL=redis://localhost:6379
# Optional - Configuration
NODE_ENV=production
LOG_LEVEL=info
CACHE_TTL=300
# Optional - JWT Validation
JWT_ISSUER=myuoj-auth
JWT_AUDIENCE=myuoj-services
# Optional - Development
AUTH_NODE_ALLOW_INTERNAL_URLS=falsePerformance
Benchmarks
| Metric | Without Cache | With Cache | Improvement | | --------------------- | ------------- | --------------- | ----------------- | | Latency | 100-200ms | 5-10ms | 95% faster | | Throughput | 100-200 req/s | 1000-2000 req/s | 10x increase | | Auth Service Load | 100% | 10% | 90% reduction | | Cache Hit Rate | N/A | 80-95% | After warm-up |
Resource Usage
- Memory: 50-100 MB baseline + cache overhead
- CPU: <5% on modern systems
- Network: 60-70% reduction with keep-alive connections
Troubleshooting
Common Issues
"Authentication required" (401)
// Check token is sent correctly
// Cookie: access_token=<jwt>
// Header: Authorization: Bearer <jwt>
// Verify cookie name matches config
const auth = authNode.init({
cookieName: "accessToken", // Default
// ...
});"Invalid or expired token" (401)
// Check JWT configuration matches auth service
const auth = authNode.init({
jwtSecret: process.env.JWT_SECRET, // Must match auth service
jwtIssuer: "myuoj-auth", // Must match token
jwtAudience: "myuoj-services", // Must match token
});High cache miss rate
// Increase TTL if users have stable permissions
const auth = authNode.init({
cacheTTL: 600, // 10 minutes instead of 5
// ...
});
// Check Redis connection
redis.on("error", (err) => {
console.error("Redis error:", err);
});Auth service unreachable
// Verify URL and network
await auth.checkAuthServiceHealth();
// Check health status
const health = await auth.getHealthStatus();
console.log(health);Debug Mode
// Enable detailed logging (development only)
const auth = authNode.init({
debug: true,
logLevel: "debug",
// ...
});
// Access logger
const { logger } = require("@uoj-lk/auth-node");
logger.setLogLevel("debug");Documentation
- 📖 Architecture & Process Flow - Deep dive into internals
- 🔒 Security Guide - Best practices and threat model
- ✅ Security Audit - Resolved vulnerabilities
- 🚀 Production Deployment - Deployment checklist
- 📋 Changelog - Version history
Related Packages
- @uoj-lk/auth-react - React authentication components
Support
- Documentation: https://github.com/UoJ-LK/myuoj-auth/tree/main/auth-node
- Issues: https://github.com/UoJ-LK/myuoj-auth/issues
- NPM: https://www.npmjs.com/package/@uoj-lk/auth-node
License
MIT © University of Jaffna
Contributing
Contributions are welcome! Please read our Contributing Guide for details.
Built with ❤️ at the University of Jaffna
